diff --git a/align_subtitles.py b/align_subtitles.py index 491ec75..f12816a 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/t_for_time/vo/' + url.split('/')[-2].split('subtitles_')[-1] + prefix = '/srv/p_for_power/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/t_for_time/vo/%s_%s.txt' % (batch, id) + txt = '/srv/p_for_power/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 58cc505..d295fbc 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": "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.", + "description": "P for Power - pan.do/ra", "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": "t_for_time", - "name": "T for Time", + "id": "p_for_power", + "name": "P for Power", // Set to true to allow search engines to index the site "public": false, "sendReferrer": true, - "url": "time.0x2620.org" + "url": "power.0x2620.org" }, /* "sitePages" defines the sections of the main site dialog. If "news" is @@ -1052,9 +1052,6 @@ 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 1ad6f4d..20a4484 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('(LOCAL_APPS.*?)\]', re.DOTALL).findall(local_settings)[0] + apps = re.compile(r'(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 4c96377..c0a16df 100644 --- a/management/commands/generate_clips.py +++ b/management/commands/generate_clips.py @@ -1,155 +1,13 @@ -import json -import os -import re -import subprocess -from collections import defaultdict - 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)) +from ...render import generate_clips, default_prefix class Command(BaseCommand): help = 'generate symlinks to clips and clips.json' def add_arguments(self, parser): - 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') + parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, help='prefix to build clips in') def handle(self, **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) + return generate_clips(options) diff --git a/management/commands/infinity.py b/management/commands/infinity.py index 0ca70fa..64ea7af 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 +from ...render import render_infinity, default_prefix class Command(BaseCommand): help = 'render infinity' def add_arguments(self, parser): - parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') + parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, 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 91e8fa5..b4a242c 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 +from ...render import render_all, default_prefix class Command(BaseCommand): help = 'genrate kdenlive porject and render' def add_arguments(self, parser): - parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') + parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, 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 a1482d6..e103375 100644 --- a/management/commands/update_subtitles.py +++ b/management/commands/update_subtitles.py @@ -5,16 +5,15 @@ import subprocess from django.core.management.base import BaseCommand from django.conf import settings -from ...render import update_subtitles +from ...render import update_subtitles, default_prefix class Command(BaseCommand): help = 'genrate kdenlive porject and render' def add_arguments(self, parser): - parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') + parser.add_argument('--prefix', action='store', dest='prefix', default=default_prefix, 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 e6d630f..8f10cd9 100644 --- a/player/play-back.desktop +++ b/player/play-back.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Exec=/srv/pandora/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u +Exec=/srv/pandora/p_for_power/player/player.py --mode peer --playlist /srv/p_for_power/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 6f95628..5d2337c 100644 --- a/player/play-front.desktop +++ b/player/play-front.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Exec=/srv/pandora/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u +Exec=/srv/pandora/p_for_power/player/player.py --mode main --playlist /srv/p_for_power/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 4804318..8a19a62 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/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u +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 [Install] WantedBy=graphical-session.target diff --git a/player/player-front.service b/player/player-front.service index 7470316..17f998b 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/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u +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 [Install] WantedBy=graphical-session.target diff --git a/player/player.py b/player/player.py index 28b41e0..fa3d548 100755 --- a/player/player.py +++ b/player/player.py @@ -13,7 +13,7 @@ import mpv import logging -logger = logging.getLogger('t_for_time') +logger = logging.getLogger('p_for_power') 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/t_for_time/render/Saxophone-5.1.mp4") + self.sax.play("/srv/p_for_power/render/Saxophone-5.1.mp4") else: self.sax = None @@ -408,11 +408,11 @@ class Sync(Thread): def main(): - prefix = os.path.expanduser('~/Videos/t_for_time') + prefix = os.path.expanduser('~/Videos/p_for_power') - parser = argparse.ArgumentParser(description='t_for_time sync player') + parser = argparse.ArgumentParser(description='p_for_power sync player') parser.add_argument('--mode', help='peer or main', default="peer") - parser.add_argument('--playlist', default='/srv/t_for_time/render/128/front.m3u', help="m3u") + parser.add_argument('--playlist', default='/srv/p_for_power/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 deleted file mode 100644 index 7b31def..0000000 --- a/player/saxophone-loop.desktop +++ /dev/null @@ -1,8 +0,0 @@ -[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 deleted file mode 100644 index 2b9d2f0..0000000 --- a/player/saxophone.service +++ /dev/null @@ -1,11 +0,0 @@ -[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 56fc94a..cbf8828 100644 --- a/render.py +++ b/render.py @@ -16,6 +16,7 @@ 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 @@ -61,7 +62,7 @@ def write_if_new(path, data, mode=''): old = "" is_new = data != old if path.endswith(".kdenlive"): - is_new = re.sub('\{.{36}\}', '', data) != re.sub('\{.{36}\}', '', old) + 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) @@ -371,17 +372,33 @@ 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": - path = os.path.join(root, prefix + "front.srt") - data = fix_overlaps(data) - srt = ox.srt.encode(data) - write_if_new(path, srt, 'b') + folder = Path(root) / prefix + write_subtitles(data, folder, options) continue #print(timeline) project = KDEnliveProject(root) @@ -469,15 +486,32 @@ 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']): - if clip['original'] in originals: + 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']) @@ -767,7 +801,7 @@ def render_all(options): fn = base_prefix / fn if os.path.exists(fn): os.unlink(fn) - join_subtitles(base_prefix) + join_subtitles(base_prefix, options) print("Duration - Target: %s Actual: %s" % (target_position, position)) print(json.dumps(dict(stats), sort_keys=True, indent=2)) @@ -790,11 +824,39 @@ def add_translations(sub, lang): value += '\n' + tvalue return value -def get_srt(sub, offset=0, lang=None): +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 lang: - sdata['value'] = add_translations(sub, lang) + 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 @@ -811,21 +873,55 @@ 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 = 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 + lang, tlang = parse_lang(options["lang"]) _cache = os.path.join(prefix, "cache.json") if os.path.exists(_cache): @@ -840,25 +936,59 @@ def update_subtitles(options): continue with open(scene_json) as fd: scene = json.load(fd) - 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') + 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")) @@ -931,7 +1061,8 @@ def render_infinity(options): shutil.move(state_f + "~", state_f) -def join_subtitles(base_prefix): +def join_subtitles(base_prefix, options): + ''' subtitles = list(sorted(glob('%s/*/front.srt' % base_prefix))) data = [] position = 0 @@ -941,3 +1072,142 @@ def join_subtitles(base_prefix): 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 deleted file mode 100644 index c1e916b..0000000 --- a/sax.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/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.t_for_time.js b/static/js/infoView.p_for_power.js similarity index 100% rename from static/js/infoView.t_for_time.js rename to static/js/infoView.p_for_power.js