#!/usr/bin/python3 from collections import defaultdict from glob import glob import json import os import re import shutil import subprocess import sys 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 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: options = {} fps = 24 length = 0 scene = { 'front': { 'V1': [], 'V2': [], }, 'back': { 'V1': [], 'V2': [], }, 'audio-back': { 'A1': [], }, 'audio-center': { 'A1': [], }, 'audio-front': { 'A1': [], 'A2': [], 'A3': [], 'A4': [], }, 'audio-rear': { 'A1': [], 'A2': [], 'A3': [], 'A4': [], }, } all_clips = clips.copy() seq = random(10000 + base * 1000) used = [] voice_overs = [] sub_offset = 0 if voice_over: vo_keys = list(sorted(voice_over)) if chance(seq, 0.5): vo_key = vo_keys[random_int(seq, len(vo_keys))] voice_overs.append(voice_over[vo_key]) elif len(vo_keys) >= 2: vo1 = vo_keys.pop(random_int(seq, len(vo_keys))) vo2 = vo_keys.pop(random_int(seq, len(vo_keys))) voice_overs.append(voice_over[vo1]) if voice_over[vo1]["duration"] + voice_over[vo2]["duration"] < target: print("adding second vo") voice_overs.append(voice_over[vo2]) print("vo:", [x['src'] for x in voice_overs], list(sorted(voice_over))) vo_min = sum([vo['duration'] for vo in voice_overs]) sub_offset = 0 if vo_min > target: target = vo_min elif vo_min < target: offset = format_duration((target - vo_min) / 2, fps) scene['audio-center']['A1'].append({ 'blank': True, 'duration': offset }) scene['audio-rear']['A1'].append({ 'blank': True, 'duration': offset }) vo_min += offset sub_offset = offset subs = [] for vo in voice_overs: voc = vo.copy() a, b = '-11', '-3' if 'Whispered' in voc['src']: a, b = '-8', '0' elif 'Read' in voc['src']: a, b = '-7.75', '0.25' elif 'Free' in voc['src']: a, b = '-8.8', '-0.8' elif 'Ashley' in voc['src']: a, b = '-9.5', '-1.50' elif 'Melody' in voc['src']: a, b = '-5.25', '-0.25' if options.get('stereo_downmix'): a, b = '-9', '-1' if 'Whispered' in voc['src']: a, b = '-6', '2' elif 'Read' in voc['src']: a, b = '-5.75', '2.25' elif 'Free' in voc['src']: a, b = '-6.8', '3.2' elif 'Ashley' in voc['src']: a, b = '-7.5', '0.50' elif 'Melody' in voc['src']: a, b = '-3.25', '1.75' voc['filter'] = {'volume': a} scene['audio-center']['A1'].append(voc) vo_low = vo.copy() vo_low['filter'] = {'volume': b} scene['audio-rear']['A1'].append(vo_low) for sub in voc.get("subs", []): sub = sub.copy() sub["in"] += sub_offset sub["out"] += sub_offset subs.append(sub) sub_offset += voc["duration"] if subs: scene["subtitles"] = subs clip = None while target - length > 0 and clips: # coin flip which site is visible (50% chance) if length: remaining = target - length remaining = remaining * 1.05 # allow for max of 10% over time clips_ = [c for c in clips if c['duration'] <= remaining] if clips_: clips = clips_ if clip: if chance(seq, 0.5): next_seqid = clip['seqid'] + 1 clip = get_clip_by_seqid(clips, next_seqid) else: clip = None if not clip: clip = random_choice(seq, clips, True) if not clips: print("not enough clips, need to reset") clips = [c for c in all_clips if c != clip and c not in used] if not clips: print("not enough clips, also consider used") clips = [c for c in all_clips if c != clip] if not clips: print("not enough clips, also consider last clip") clips = all_clips.copy() if length + clip['duration'] > target and length >= vo_min: break print('%06.3f %06.3f' % (length, clip['duration']), os.path.basename(clip['original'])) length += int(clip['duration'] * fps) / fps if "foreground" not in clip and "animation" in clip: fg = clip['animation'] transparancy = 1 else: fg = clip['foreground'] if 'animation' in clip and chance(seq, 0.15): fg = clip['animation'] transparancy = 1 else: if 'foreground2' in clip: if 'foreground3' in clip: n = seq() if n <= 3: # 0,1,2,3 clip['foreground'] elif n <= 6: # 4,5,6 clip['foreground2'] else: # 7,8,9 clip['foreground3'] elif chance(seq, 0.5): fg = clip['foreground2'] transparancy = seq() / 9 transparancy = 1 if 'foley' in clip: foley = clip['foley'] else: foley = fg scene['front']['V2'].append({ 'duration': clip['duration'], 'src': fg, "filter": { 'transparency': transparancy, } }) transparency = seq() / 9 # 50% of time no transparancy of foregroudnd layer # 50% some transparancy, 25%, 50%, 75% levels of transparancy transparancy = 1 # coin flip which site is visible (50% chance) #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 if "background" in clip: scene['front']['V1'].append({ 'duration': clip['duration'], 'src': clip['background'], "filter": { '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'], 'src': clip['animation'], "filter": { '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({ 'duration': clip['duration'], 'src': foley, 'filter': {'volume': cf_volume}, }) scene['audio-rear']['A2'].append({ 'duration': clip['duration'], 'src': foley, 'filter': {'volume': cf_volume}, }) used.append(clip) print("scene duration %0.3f (target: %0.3f, vo_min: %0.3f)" % (length, target, vo_min)) scene_duration = int(get_scene_duration(scene) * fps) sub_offset = int(sub_offset * fps) if sub_offset < scene_duration: delta = format_duration((scene_duration - sub_offset) / fps, fps) print(">> add %0.3f of silence.. %0.3f (scene_duration)" % (delta, scene_duration / fps)) scene['audio-center']['A1'].append({ 'blank': True, 'duration': delta }) scene['audio-rear']['A1'].append({ 'blank': True, 'duration': delta }) elif sub_offset > scene_duration: delta = format_duration((scene_duration - sub_offset) / fps, fps) scene['audio-center']['A1'][-1]["duration"] += delta scene['audio-rear']['A1'][-1]["duration"] += delta print("WTF, needed to cut %s new duration: %s" % (delta, scene['audio-center']['A1'][-1]["duration"])) 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" if options.get("subtitle_format") == "srt": srt = ox.srt.encode(data) write_if_new(str(path), srt, 'b') path = folder / "front.ass" if os.path.exists(path): os.unlink(path) else: if os.path.exists(path): os.unlink(path) path = folder / "front.ass" ass = ass_encode(data, options) write_if_new(str(path), ass, '') def render(root, scene, prefix='', options=None): if options is None: options = {} fps = 24 files = [] scene_duration = int(get_scene_duration(scene) * fps) for timeline, data in scene.items(): if timeline == "subtitles": folder = Path(root) / prefix write_subtitles(data, folder, options) continue #print(timeline) project = KDEnliveProject(root) tracks = [] track_durations = {} for track, clips in data.items(): #print(track) for clip in clips: project.append_clip(track, clip) track_durations[track] = sum([int(c['duration'] * fps) for c in clips]) if timeline.startswith('audio-'): track_duration = project.get_duration() delta = scene_duration - track_duration if delta > 0: for track in track_durations: if track_durations[track] == track_duration: project.append_clip(track, {'blank': True, "duration": delta/fps}) path = os.path.join(root, prefix + "%s.kdenlive" % timeline) project_xml = project.to_xml() write_if_new(path, project_xml) if options["debug"]: # check duration out_duration = get_project_duration(path) p_duration = project.get_duration() print(path, 'out: %s, project: %s, scene: %s' %(out_duration, p_duration, scene_duration)) if p_duration != scene_duration: print(path, 'FAIL project: %s, scene: %s' %(p_duration, scene_duration)) _cache = os.path.join(root, "cache.json") with open(_cache, "w") as fd: json.dump(_CACHE, fd) sys.exit(1) if out_duration != p_duration: print(path, 'fail got: %s expected: %s' %(out_duration, p_duration)) sys.exit(1) 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 fragments = [] for l in itemlist.models.List.objects.filter(status='featured').order_by('name'): if l.name.split(' ')[0].isdigit(): fragment = { 'name': l.name, 'tags': [], 'anti-tags': [], 'description': l.description } for con in l.query['conditions']: if "conditions" in con: for sub in con["conditions"]: if sub['key'] == "tags" and sub['operator'] == '==': fragment['tags'].append(sub['value']) elif sub['key'] == "tags" and sub['operator'] == '!=': fragment['tags'].append(sub['value']) else: print(l.name, 'unknown sub condition', sub) elif con.get('key') == "tags" and con['operator'] == '==': fragment['tags'].append(con['value']) elif con.get('key') == "tags" and con['operator'] == '!=': fragment['anti-tags'].append(con['value']) fragment["id"] = int(fragment['name'].split(' ')[0]) originals = [] for i in l.get_items(l.user): orig = i.files.filter(selected=True).first() if orig: ext = os.path.splitext(orig.data.path)[1] type_ = i.data['type'][0].lower() target = os.path.join(prefix, type_, i.data['title'] + ext) originals.append(target) fragment['clips'] = [] for clip in clips: #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"]), {}) fragments.append(fragment) 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'] duration = int(options['duration']) base = int(options['offset']) _cache = os.path.join(prefix, "cache.json") if os.path.exists(_cache): with open(_cache) as fd: _CACHE.update(json.load(fd)) with open(os.path.join(prefix, "clips.json")) as fd: clips = json.load(fd) with open(os.path.join(prefix, "voice_over.json")) as fd: voice_over = json.load(fd) fragments = get_fragments(clips, voice_over, prefix) with open(os.path.join(prefix, "fragments.json"), "w") as fd: json.dump(fragments, fd, indent=2, ensure_ascii=False) position = target_position = 0 target = fragment_target = duration / len(fragments) base_prefix = os.path.join(prefix, 'render', str(base)) clips_used = [] stats = defaultdict(lambda: 0) fragment_base = base for fragment in fragments: fragment_base += 1 fragment_id = int(fragment['name'].split(' ')[0]) name = fragment['name'].replace(' ', '_') if fragment_id < 10: name = '0' + name if not fragment['clips']: print("skipping empty fragment", name) continue fragment_prefix = os.path.join(base_prefix, name) os.makedirs(fragment_prefix, exist_ok=True) fragment_clips = fragment['clips'] unused_fragment_clips = [c for c in fragment_clips if c not in clips_used] print('fragment clips', len(fragment_clips), 'unused', len(unused_fragment_clips)) scene, used = compose( unused_fragment_clips, target=target, base=fragment_base, voice_over=fragment['voice_over'], options=options ) clips_used += used scene_duration = get_scene_duration(scene) print("%s %6.3f -> %6.3f (%6.3f)" % (name, target, scene_duration, fragment_target)) src = [a for a in scene['audio-rear']['A1'] if 'src' in a][0]['src'] stats[src.split('/')[-2]] += 1 position += scene_duration target_position += fragment_target if position > target_position: target = fragment_target - (position-target_position) print("adjusting target duration for next fragment: %6.3f -> %6.3f" % (fragment_target, target)) elif position < target_position: target = target + 0.1 * fragment_target timelines = render(prefix, scene, fragment_prefix[len(prefix) + 1:] + '/', 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'] and not options["single_file"]: for timeline in timelines: print(timeline) ext = '.mp4' if '/audio' in timeline: ext = '.wav' cmd = get_melt() + [ timeline, '-quiet', '-consumer', 'avformat:%s' % timeline.replace('.kdenlive', ext), ] 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) if ext == '.wav' and timeline.endswith('audio.kdenlive'): cmd = [ 'ffmpeg', '-y', '-nostats', '-loglevel', 'error', '-i', timeline.replace('.kdenlive', ext), timeline.replace('.kdenlive', '.mp4') ] subprocess.call(cmd) os.unlink(timeline.replace('.kdenlive', ext)) 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"), ("audio-rear.wav", "bl.wav", "br.wav"), ): cmds.append([ "ffmpeg", "-y", "-nostats", "-loglevel", "error", "-i", fragment_prefix / src, "-filter_complex", "[0:0]pan=1|c0=c0[left]; [0:0]pan=1|c0=c1[right]", "-map", "[left]", fragment_prefix / out1, "-map", "[right]", fragment_prefix / out2, ]) cmds.append([ "ffmpeg", "-y", "-nostats", "-loglevel", "error", "-i", fragment_prefix / "fl.wav", "-i", fragment_prefix / "fr.wav", "-i", fragment_prefix / "fc.wav", "-i", fragment_prefix / "lfe.wav", "-i", fragment_prefix / "bl.wav", "-i", fragment_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", 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([ "ffmpeg", "-y", "-nostats", "-loglevel", "error", "-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([ "ffmpeg", "-y", "-nostats", "-loglevel", "error", "-i", fragment_prefix / "front.mp4", "-i", fragment_prefix / audio_front, copy, "copy", "-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'] duration_b = ox.avinfo(str(fragment_prefix / b))['duration'] if duration_a != duration_b: 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", "fl.wav", "fr.wav", "fc.wav", "lfe.wav", "bl.wav", "br.wav", "audio-stereo.wav", ): fn = fragment_prefix / fn 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 = get_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", "-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", "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, options) 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: json.dump(_CACHE, fd) def add_translations(sub, lang): value = sub.value.replace('
', '
').replace('
\n', '\n').replace('
', '\n').strip() if sub.languages: value = ox.strip_tags(value) if lang: for slang in lang: if slang == "en": slang = None for tsub in sub.item.annotations.filter(layer="subtitles", start=sub.start, end=sub.end, languages=slang): tvalue = tsub.value.replace('
', '
').replace('
\n', '\n').replace('
', '\n').strip() if tsub.languages: tvalue = ox.strip_tags(tvalue) value += '\n' + tvalue return value def add_translations_dict(sub, langs): values = {} value = sub.value.replace('
', '
').replace('
\n', '\n').replace('
', '\n').strip() if sub.languages: value = ox.strip_tags(value) values[sub.languages] = value else: values["en"] = value for slang in langs: slang_value = None if slang == "en" else slang if sub.languages == slang_value: continue for tsub in sub.item.annotations.filter( layer="subtitles", start=sub.start, end=sub.end, languages=slang_value ): tvalue = tsub.value.replace('
', '
').replace('
\n', '\n').replace('
', '\n').strip() if tsub.languages: tvalue = ox.strip_tags(tvalue) 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() if tlang: sdata['value'] = add_translations(sub, tlang) langs = [lang] if tlang: langs += tlang sdata['values'] = add_translations_dict(sub, langs) if offset: sdata["in"] += offset 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 def scene_subtitles(scene, options): import item.models offset = 0 subs = [] lang, tlang = parse_lang(options["lang"]) for clip in scene['audio-center']['A1']: if not clip.get("blank"): batch, fragment_id = clip['src'].replace('.wav', '').split('/')[-2:] vo = item.models.Item.objects.filter( data__batch__icontains=batch, data__title__startswith=fragment_id + '_' ).first() if vo: #print("%s => %s %s" % (clip['src'], vo, vo.get('batch'))) for sub in vo.annotations.filter( layer="subtitles" ).filter( languages=None if lang == "en" else lang ).exclude(value="").order_by("start"): sdata = get_srt(sub, offset, lang, tlang) subs.append(sdata) else: print("could not find vo for %s" % clip['src']) offset += clip['duration'] return subs def load_defaults(options): path = os.path.join(options["prefix"], "options.json") if os.path.exists(path): with open(path) as fd: defaults = json.load(fd) for key in defaults: if key not in options: options[key] = defaults[key] return options def update_subtitles(options): import item.models options = load_defaults(options) prefix = Path(options['prefix']) base = int(options['offset']) lang, tlang = parse_lang(options["lang"]) _cache = os.path.join(prefix, "cache.json") if os.path.exists(_cache): with open(_cache) as fd: _CACHE.update(json.load(fd)) base_prefix = prefix / 'render' / str(base) for folder in os.listdir(base_prefix): folder = base_prefix / folder scene_json = folder / "scene.json" if not os.path.exists(scene_json): continue with open(scene_json) as fd: scene = json.load(fd) 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")) 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") 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'] duration = int(options['duration']) defaults = { "offset": 100, "max-items": 30, "no_video": False, } state_f = os.path.join(prefix, "infinity.json") if os.path.exists(state_f): with open(state_f) as fd: state = json.load(fd) else: state = {} for key in ("prefix", "duration", "debug", "single_file", "keep_audio", "stereo_downmix"): state[key] = options[key] for key in defaults: if key not in state: state[key] = defaults[key] while True: render_prefix = state["prefix"] + "/render/" current = [ f for f in os.listdir(render_prefix) if f.isdigit() and os.path.isdir(render_prefix + f) and state["offset"] > int(f) >= 100 ] if len(current) > state["max-items"]: current = ox.sorted_strings(current) remove = current[:-state["max-items"]] update_m3u(render_prefix, exclude=remove) for folder in remove: 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: json.dump(state, fd, indent=2) shutil.move(state_f + "~", state_f) 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 for scene in scenes: subs = scene_subtitles(scene, options) data += shift_clips(subs, position) 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 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 = "" qs = item.models.Item.objects.filter(data__title=i.data['title']).exclude(id=i.id) if qs.count() >= 1: clip = {} durations = [] for e in item.models.Item.objects.filter(data__title=i.data['title']): if 'type' not in e.data: print("ignoring invalid video %s (no type)" % e) continue if not e.files.filter(selected=True).exists(): continue source = e.files.filter(selected=True)[0].data.path ext = os.path.splitext(source)[1] type_ = e.data['type'][0].lower() target = os.path.join(prefix, type_, i.data['title'] + ext) os.makedirs(os.path.dirname(target), exist_ok=True) if os.path.islink(target): os.unlink(target) 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) if not clip["duration"]: 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) seqid = re.sub(r"_(\d+)H_(\d+)", "_S\\1\\2_", seqid) seqid = seqid.split('_')[:2] seqid = [b[1:] if b[0] in ('B', 'S') else '0' for b in seqid] seqid[1] = resolve_roman(seqid[1]) seqid[1] = ''.join([b for b in seqid[1] if b.isdigit()]) if not seqid[1]: seqid[1] = '0' try: clip['seqid'] = int(''.join(['%06d' % int(b) for b in seqid])) 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) with open(os.path.join(prefix, 'clips.json'), 'w') as fd: json.dump(clips, fd, indent=2, ensure_ascii=False) print("using", len(clips), "clips") voice_over = defaultdict(dict) for vo in item.models.Item.objects.filter( data__type__contains="Voice Over", ): fragment_id = int(vo.get('title').split('_')[0]) source = vo.files.filter(selected=True)[0] batch = vo.get('batch')[0].replace('Text-', '') src = source.data.path target = os.path.join(prefix, 'voice_over', batch, '%s.wav' % fragment_id) os.makedirs(os.path.dirname(target), exist_ok=True) if os.path.islink(target): os.unlink(target) os.symlink(src, target) subs = [] for sub in vo.annotations.filter( layer="subtitles", languages=lang ).exclude(value="").order_by("start"): sdata = get_srt(sub, 0, lang, tlang) subs.append(sdata) voice_over[fragment_id][batch] = { "src": target, "duration": format_duration(source.duration, 24), "subs": subs } 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)