diff --git a/align_subtitles.py b/align_subtitles.py index f12816a..491ec75 100644 --- a/align_subtitles.py +++ b/align_subtitles.py @@ -21,7 +21,7 @@ https://textb.org/r/t_for_time_subtitles_5_ashley/ data = requests.get(url).text parts = data.strip().split('##') print(url) - prefix = '/srv/p_for_power/vo/' + url.split('/')[-2].split('subtitles_')[-1] + prefix = '/srv/t_for_time/vo/' + url.split('/')[-2].split('subtitles_')[-1] for part in parts: part = part.strip().split('\n') if part: @@ -86,7 +86,7 @@ def update_subtitles(): wav = i.files.filter(selected=True)[0].data.path id = i.get('title').split('_')[0] batch = i.get('batch')[0][5:].lower().replace('-', '_').replace(' ', '') - txt = '/srv/p_for_power/vo/%s_%s.txt' % (batch, id) + txt = '/srv/t_for_time/vo/%s_%s.txt' % (batch, id) if os.path.exists(txt): print(i, wav, txt) subtitles = gentle2subtitles(align_text(txt, wav)) diff --git a/config.jsonc b/config.jsonc index d295fbc..58cc505 100644 --- a/config.jsonc +++ b/config.jsonc @@ -1015,7 +1015,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. the system (from). */ "site": { - "description": "P for Power - pan.do/ra", + "description": "This is a demo of pan.do/ra - a free, open source media archive. It allows you to manage large, decentralized collections of video, to collaboratively create metadata and time-based annotations, and to serve your archive as a cutting-edge web application.", "email": { // E-mail address in contact form (to) "contact": "system@time.0x2620.org", @@ -1025,12 +1025,12 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "system": "system@time.0x2620.org" }, "https": true, - "id": "p_for_power", - "name": "P for Power", + "id": "t_for_time", + "name": "T for Time", // Set to true to allow search engines to index the site "public": false, "sendReferrer": true, - "url": "power.0x2620.org" + "url": "time.0x2620.org" }, /* "sitePages" defines the sections of the main site dialog. If "news" is @@ -1052,6 +1052,9 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. cross-instance references. */ "sites": [ + {"name": "0xDB", "url": "0xdb.org", "https": true}, + {"name": "Pad.ma", "url": "pad.ma", "https": true}, + {"name": "Indiancine.ma", "url": "indiancine.ma", "https": true} ], /* "textRightsLevels" defines a list of rights levels for texts. diff --git a/install.py b/install.py index 20a4484..1ad6f4d 100755 --- a/install.py +++ b/install.py @@ -100,7 +100,7 @@ if os.path.exists('__init__.py'): local_settings += '\nLOCAL_APPS = ["%s"]\n' % name local_settings_changed = True else: - apps = re.compile(r'(LOCAL_APPS.*?)\]', re.DOTALL).findall(local_settings)[0] + apps = re.compile('(LOCAL_APPS.*?)\]', re.DOTALL).findall(local_settings)[0] if name not in apps: new_apps = apps.strip() + ',\n"%s"\n' % name local_settings = local_settings.replace(apps, new_apps) diff --git a/management/commands/generate_clips.py b/management/commands/generate_clips.py index c0a16df..4c96377 100644 --- a/management/commands/generate_clips.py +++ b/management/commands/generate_clips.py @@ -1,13 +1,155 @@ -from django.core.management.base import BaseCommand +import json +import os +import re +import subprocess +from collections import defaultdict -from ...render import generate_clips, default_prefix +from django.core.management.base import BaseCommand +from django.conf import settings + +import item.models +import itemlist.models + +from ...render import get_srt + + +def resolve_roman(s): + extra = re.compile('^\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 format_duration(duration, fps): + return float('%0.5f' % (round(duration * fps) / fps)) class Command(BaseCommand): help = 'generate symlinks to clips and clips.json' def add_arguments(self, parser): - parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, help='prefix to build clips in') + parser.add_argument('--lang', action='store', dest='lang', default=None, help='subtitle language') + parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') + parser.add_argument('--censored', action='store', dest='censored', default=None, help='censor items from list') def handle(self, **options): - return generate_clips(options) + prefix = options['prefix'] + lang = options["lang"] + if lang: + lang = lang.split(',') + tlang = lang[1:] + lang = lang[0] + else: + tlang = None + if lang == "en": + lang = None + 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: + 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("Hotel Aporia_(\d+)", "S\\1_", name) + seqid = re.sub("Night March_(\d+)", "S\\1_", seqid) + seqid = re.sub("_(\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, 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) diff --git a/management/commands/infinity.py b/management/commands/infinity.py index 64ea7af..0ca70fa 100644 --- a/management/commands/infinity.py +++ b/management/commands/infinity.py @@ -5,14 +5,14 @@ import subprocess from django.core.management.base import BaseCommand from django.conf import settings -from ...render import render_infinity, default_prefix +from ...render import render_infinity class Command(BaseCommand): help = 'render infinity' def add_arguments(self, parser): - parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, help='prefix to build clips in') + parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') parser.add_argument('--config', action='store', dest='config', default=None, help='config') parser.add_argument('--duration', action='store', dest='duration', default="3600", help='target duration of all fragments in seconds') parser.add_argument('--single-file', action='store_true', dest='single_file', default=False, help='render to single video') diff --git a/management/commands/render.py b/management/commands/render.py index b4a242c..91e8fa5 100644 --- a/management/commands/render.py +++ b/management/commands/render.py @@ -5,14 +5,14 @@ import subprocess from django.core.management.base import BaseCommand from django.conf import settings -from ...render import render_all, default_prefix +from ...render import render_all class Command(BaseCommand): help = 'genrate kdenlive porject and render' def add_arguments(self, parser): - parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, help='prefix to build clips in') + parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') 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') diff --git a/management/commands/update_subtitles.py b/management/commands/update_subtitles.py index e103375..a1482d6 100644 --- a/management/commands/update_subtitles.py +++ b/management/commands/update_subtitles.py @@ -5,15 +5,16 @@ import subprocess from django.core.management.base import BaseCommand from django.conf import settings -from ...render import update_subtitles, default_prefix +from ...render import update_subtitles class Command(BaseCommand): help = 'genrate kdenlive porject and render' def add_arguments(self, parser): - parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, help='prefix to build clips in') + parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') parser.add_argument('--offset', action='store', dest='offset', default="1024", help='inital offset in pi') + parser.add_argument('--lang', action='store', dest='lang', default=None, help='subtitle language') def handle(self, **options): update_subtitles(options) diff --git a/player/play-back.desktop b/player/play-back.desktop index 8f10cd9..e6d630f 100644 --- a/player/play-back.desktop +++ b/player/play-back.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Exec=/srv/pandora/p_for_power/player/player.py --mode peer --playlist /srv/p_for_power/render/back.m3u +Exec=/srv/pandora/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u Hidden=false NoDisplay=false X-GNOME-Autostart-enabled=true diff --git a/player/play-front.desktop b/player/play-front.desktop index 5d2337c..6f95628 100644 --- a/player/play-front.desktop +++ b/player/play-front.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Exec=/srv/pandora/p_for_power/player/player.py --mode main --playlist /srv/p_for_power/render/front.m3u +Exec=/srv/pandora/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u Hidden=false NoDisplay=false X-GNOME-Autostart-enabled=true diff --git a/player/player-back.service b/player/player-back.service index 8a19a62..4804318 100644 --- a/player/player-back.service +++ b/player/player-back.service @@ -7,7 +7,7 @@ Wants=network-online.target Type=simple Restart=on-failure KillSignal=SIGINT -ExecStart=/srv/pandora/p_for_power/player/player.py --mode peer --playlist /srv/p_for_power/render/back.m3u --config /srv/p_for_power/render/back.json +ExecStart=/srv/pandora/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u [Install] WantedBy=graphical-session.target diff --git a/player/player-front.service b/player/player-front.service index 17f998b..7470316 100644 --- a/player/player-front.service +++ b/player/player-front.service @@ -6,7 +6,7 @@ After=gnome-session.target network-online.target Type=simple Restart=on-failure KillSignal=SIGINT -ExecStart=/srv/pandora/p_for_power/player/player.py --mode main --playlist /srv/p_for_power/render/front.m3u --config /srv/p_for_power/render/front.json +ExecStart=/srv/pandora/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u [Install] WantedBy=graphical-session.target diff --git a/player/player.py b/player/player.py index fa3d548..28b41e0 100755 --- a/player/player.py +++ b/player/player.py @@ -13,7 +13,7 @@ import mpv import logging -logger = logging.getLogger('p_for_power') +logger = logging.getLogger('t_for_time') SYNC_TOLERANCE = 0.05 SYNC_GRACE_TIME = 5 @@ -74,7 +74,7 @@ class Sync(Thread): input_vo_keyboard=True, ) self.sax.loop_file = True - self.sax.play("/srv/p_for_power/render/Saxophone-5.1.mp4") + self.sax.play("/srv/t_for_time/render/Saxophone-5.1.mp4") else: self.sax = None @@ -408,11 +408,11 @@ class Sync(Thread): def main(): - prefix = os.path.expanduser('~/Videos/p_for_power') + prefix = os.path.expanduser('~/Videos/t_for_time') - parser = argparse.ArgumentParser(description='p_for_power sync player') + parser = argparse.ArgumentParser(description='t_for_time sync player') parser.add_argument('--mode', help='peer or main', default="peer") - parser.add_argument('--playlist', default='/srv/p_for_power/render/128/front.m3u', help="m3u") + parser.add_argument('--playlist', default='/srv/t_for_time/render/128/front.m3u', help="m3u") parser.add_argument('--prefix', help='video location', default=prefix) parser.add_argument('--window', action='store_true', help='run in window', default=False) parser.add_argument('--debug', action='store_true', help='debug', default=False) diff --git a/player/saxophone-loop.desktop b/player/saxophone-loop.desktop new file mode 100644 index 0000000..7b31def --- /dev/null +++ b/player/saxophone-loop.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Exec=/usr/bin/mpv --quiet --loop /srv/t_for_time/render/Saxophone-5.1.mp4 +Hidden=false +NoDisplay=false +X-GNOME-Autostart-enabled=true +Name=loop +Comment= diff --git a/player/saxophone.service b/player/saxophone.service new file mode 100644 index 0000000..2b9d2f0 --- /dev/null +++ b/player/saxophone.service @@ -0,0 +1,11 @@ +[Unit] +Description=saxophone loop + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/mpv --quiet --loop /srv/t_for_time/render/Saxophone-5.1.mp4 +KillSignal=SIGINT + +[Install] +WantedBy=graphical-session.target diff --git a/render.py b/render.py index cbf8828..56fc94a 100644 --- a/render.py +++ b/render.py @@ -16,7 +16,6 @@ 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 @@ -62,7 +61,7 @@ def write_if_new(path, data, mode=''): old = "" is_new = data != old if path.endswith(".kdenlive"): - is_new = re.sub(r'\{.{36}\}', '', data) != re.sub(r'\{.{36}\}', '', old) + is_new = re.sub('\{.{36}\}', '', data) != re.sub('\{.{36}\}', '', old) if is_new: with open(path, write_mode) as fd: fd.write(data) @@ -372,33 +371,17 @@ def get_offset_duration(prefix): 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 = {} + 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) + path = os.path.join(root, prefix + "front.srt") + data = fix_overlaps(data) + srt = ox.srt.encode(data) + write_if_new(path, srt, 'b') continue #print(timeline) project = KDEnliveProject(root) @@ -486,32 +469,15 @@ def get_fragments(clips, voice_over, prefix): 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: + if clip['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']) @@ -801,7 +767,7 @@ def render_all(options): fn = base_prefix / fn if os.path.exists(fn): os.unlink(fn) - join_subtitles(base_prefix, options) + join_subtitles(base_prefix) print("Duration - Target: %s Actual: %s" % (target_position, position)) print(json.dumps(dict(stats), sort_keys=True, indent=2)) @@ -824,39 +790,11 @@ def add_translations(sub, lang): 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): +def get_srt(sub, offset=0, lang=None): 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 lang: + sdata['value'] = add_translations(sub, lang) if offset: sdata["in"] += offset sdata["out"] += offset @@ -873,55 +811,21 @@ def fix_overlaps(data): 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"]) + lang = options["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 _cache = os.path.join(prefix, "cache.json") if os.path.exists(_cache): @@ -936,59 +840,25 @@ def update_subtitles(options): continue with open(scene_json) as fd: scene = json.load(fd) - subs = scene_subtitles(scene, options) - write_subtitles(subs, folder, options) + offset = 0 + subs = [] + 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=lang).exclude(value="").order_by("start"): + sdata = get_srt(sub, offset, tlang) + subs.append(sdata) + else: + print("could not find vo for %s" % clip['src']) + offset += clip['duration'] + path = folder / "front.srt" + data = fix_overlaps(subs) + srt = ox.srt.encode(subs) + write_if_new(str(path), srt, 'b') -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")) @@ -1061,8 +931,7 @@ def render_infinity(options): shutil.move(state_f + "~", state_f) -def join_subtitles(base_prefix, options): - ''' +def join_subtitles(base_prefix): subtitles = list(sorted(glob('%s/*/front.srt' % base_prefix))) data = [] position = 0 @@ -1072,142 +941,3 @@ def join_subtitles(base_prefix, options): 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) diff --git a/sax.py b/sax.py new file mode 100644 index 0000000..c1e916b --- /dev/null +++ b/sax.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 +import os +from render_kdenlive import KDEnliveProject, _CACHE +import subprocess + +def generate_sax_mix(root): + os.chdir(root) + + +root = os.path.abspath(".") + +long_wav = "Soon_Kim_Long_Reverb_Only2.wav" +nois_wav = "Soon_Kim_Noise.wav" +reverb_wav = "Soon_Kim_Short_Reverb_Mix2.wav" +''' +i = item.models.Item.objects.get(data__title='Soon_Kim_Long_Reverb_Only2') +i.files.all()[0].data.path +'/srv/pandora/data/media/6b/44/16/3f2905e886/data.wav' + +i = item.models.Item.objects.get(data__title='Soon_Kim_Short_Reverb_Mix2') +i.files.all()[0].data.path +'/srv/pandora/data/media/ee/e0/04/d4ab42c3de/data.wav' + +i = item.models.Item.objects.get(data__title='Soon_Kim_Noise') +i.files.all()[0].data.path +'/srv/pandora/data/media/84/88/87/d2fb2e2dc2/data.wav' +''' + + +reverb = { + "src": reverb_wav, + "duration": 3600.0, + "filter": { + "volume": "3.5" + }, +} + +long = { + "src": long_wav, + "duration": 3600.0, + "filter": { + "volume": "-1" + }, +} +noise = { + "src": nois_wav, + "duration": 3600.0, + "filter": { + "volume": "7.75" + }, +} + +project = KDEnliveProject(root) +project.append_clip('A1', long) +project.append_clip('A2', noise) +path = os.path.join(root, "sax-mix.kdenlive") +with open(path, 'w') as fd: + fd.write(project.to_xml()) + +project = KDEnliveProject(root) +project.append_clip('A1', reverb) +path = os.path.join(root, "sax-reverb-mix.kdenlive") +with open(path, 'w') as fd: + fd.write(project.to_xml()) + +cmds = [] +cmds.append([ + "melt", "sax-mix.kdenlive", '-quiet', '-consumer', 'avformat:sax-mix.wav' +]) +cmds.append([ + "melt", "sax-reverb-mix.kdenlive", '-quiet', '-consumer', 'avformat:sax-reverb-mix.wav' +]) +cmds.append([ + "ffmpeg", "-y", + "-nostats", "-loglevel", "error", + "-f", "lavfi", "-i", "anullsrc=r=48000:cl=mono", "-t", "3600", "silence.wav" +]) + +for src, out1, out2 in ( + ('sax-reverb-mix.wav', "fl.wav", "fr.wav"), + ("sax-mix.wav", "bl.wav", "br.wav"), +): + cmds.append([ + "ffmpeg", "-y", + "-nostats", "-loglevel", "error", + "-i", src, + "-filter_complex", + "[0:0]pan=1|c0=c0[left]; [0:0]pan=1|c0=c1[right]", + "-map", "[left]", out1, + "-map", "[right]", out2, + ]) + +cmds.append([ + "ffmpeg", "-y", + "-nostats", "-loglevel", "error", + "-i", "fl.wav", + "-i", "fr.wav", + "-i", "silence.wav", + "-i", "silence.wav", + "-i", "bl.wav", + "-i", "br.wav", + "-filter_complex", "[0:a][1:a][2:a][3:a][4:a][5:a]amerge=inputs=6[a]", + "-map", "[a]", + "-ar", "48000", + "-c:a", "aac", "Saxophone-5.1.mp4" +]) +for cmd in cmds: + print(" ".join([str(x) for x in cmd])) + subprocess.call(cmd) diff --git a/static/js/infoView.p_for_power.js b/static/js/infoView.t_for_time.js similarity index 100% rename from static/js/infoView.p_for_power.js rename to static/js/infoView.t_for_time.js