diff --git a/config.jsonc b/config.jsonc index 79a4e2a..e598d2e 100644 --- a/config.jsonc +++ b/config.jsonc @@ -63,7 +63,6 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "canExportAnnotations": {"member": true, "staff": true, "admin": true}, "canImportAnnotations": {"member": true, "staff": true, "admin": true}, "canImportItems": {"member": true, "staff": true, "admin": true}, - "canTranscribeAudio": {}, "canManageDocuments": {"member": true, "staff": true, "admin": true}, "canManageEntities": {"member": true, "staff": true, "admin": true}, "canManageHome": {"staff": true, "admin": true}, @@ -1015,7 +1014,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. the system (from). */ "site": { - "description": "T for Time - 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", diff --git a/etc/systemd/system/render-infinity.service b/etc/systemd/system/render-infinity.service index a016046..9afaad8 100644 --- a/etc/systemd/system/render-infinity.service +++ b/etc/systemd/system/render-infinity.service @@ -9,7 +9,7 @@ User=pandora Group=pandora Nice=19 WorkingDirectory=/srv/pandora/pandora -ExecStart=/srv/pandora/pandora/manage.py infinity --config /etc/infinity.json +ExecStart=/srv/pandora/pandora/manage.py infinity [Install] WantedBy=multi-user.target 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/export_subtitles.py b/management/commands/export_subtitles.py index c954e90..1807fbb 100644 --- a/management/commands/export_subtitles.py +++ b/management/commands/export_subtitles.py @@ -2,43 +2,24 @@ import json import os import subprocess -import ox - from django.core.management.base import BaseCommand from django.conf import settings -from ...render import add_translations - class Command(BaseCommand): help = 'export all subtitles for translations' def add_arguments(self, parser): - parser.add_argument('--lang', action='store', dest='lang', default=None, help='subtitle language') + pass def handle(self, **options): + import annotation.models import item.models - lang = options["lang"] - if lang: - lang = lang.split(',') - tlang = lang[1:] - lang = lang[0] - else: - tlang = None - if lang == "en": - lang = None - for i in item.models.Item.objects.filter(data__type__contains='Voice Over').order_by('sort__title'): print("## %s %s" % (i.get("title"), i.public_id)) - for sub in i.annotations.all().filter(layer='subtitles').exclude(value='').filter(languages=lang).order_by("start"): - if tlang: - value = add_translations(sub, tlang) - value = ox.strip_tags(value) - else: - value = sub.value.replace('
', '
').replace('
\n', '\n').replace('
', '\n').strip() - if sub.languages: - value = ox.strip_tags(value) - print(value.strip() + "\n") + for sub in i.annotations.all().filter(layer='subtitles').exclude(value='').order_by("start"): + if not sub.languages: + print(sub.value.strip() + "\n") print("\n\n\n") diff --git a/management/commands/generate_clips.py b/management/commands/generate_clips.py index abfd55d..9fd1c13 100644 --- a/management/commands/generate_clips.py +++ b/management/commands/generate_clips.py @@ -1,6 +1,27 @@ -from django.core.management.base import BaseCommand +import json +import os +import re +from collections import defaultdict -from ...render import generate_clips +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 class Command(BaseCommand): @@ -10,4 +31,84 @@ class Command(BaseCommand): parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in') def handle(self, **options): - return generate_clips(options) + prefix = options['prefix'] + clips = [] + for i in item.models.Item.objects.filter(sort__type='original'): + 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) + clip[type_] = target + durations.append(e.files.filter(selected=True)[0].duration) + clip["duration"] = min(durations) + if not clip["duration"]: + print('!!', durations, clip) + continue + clip['tags'] = i.data.get('tags', []) + clip['editingtags'] = i.data.get('editingtags', []) + name = os.path.basename(clip['original']) + + 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").exclude(value="").order_by("start"): + sdata = get_srt(sub) + subs.append(sdata) + voice_over[fragment_id][batch] = { + "src": target, + "duration": source.duration, + "subs": subs + } + with open(os.path.join(prefix, 'voice_over.json'), 'w') as fd: + json.dump(voice_over, fd, indent=2, ensure_ascii=False) diff --git a/management/commands/import_subtitles.py b/management/commands/import_subtitles.py deleted file mode 100644 index 6dcfe4a..0000000 --- a/management/commands/import_subtitles.py +++ /dev/null @@ -1,109 +0,0 @@ -import json -import os -import subprocess - -import ox - -from django.core.management.base import BaseCommand -from django.conf import settings - -from item.models import Item -from annotation.models import Annotation - - -class Command(BaseCommand): - help = 'export all subtitles for translations' - - def add_arguments(self, parser): - parser.add_argument('--lang', action='store', dest='lang', default=None, help='subtitle language') - parser.add_argument('--test', action='store_true', dest='test', default=False, help='test run') - parser.add_argument('args', metavar='args', type=str, nargs='*', help='file or url') - - def handle(self, filename, **options): - if not options["lang"]: - print("--lang is required") - return - lang = options["lang"] - - if filename.startswith("http"): - data = ox.net.read_url(filename).decode() - else: - with open(filename) as fd: - data = fd.read() - - data = ('\n' + data.strip()).split('\n## ')[1:] - - invalid = [] - valid = [] - for block in data: - title, block = block.split('\n', 1) - block = block.strip() - title = title.strip() - item_id = title.split(' ')[-1] - item = Item.objects.get(public_id=item_id) - - subtitles_en = item.annotations.filter(layer="subtitles", languages=None).exclude(value='') - lines = block.split('\n\n') - if len(lines) != subtitles_en.count(): - print('%s: number of subtitles does not match, en: %s vs %s: %s' % (title, subtitles_en.count(), lang, len(lines))) - if options["test"]: - print(json.dumps(lines, indent=2, ensure_ascii=False)) - print(json.dumps([s.value for s in subtitles_en.order_by('start')], indent=2, ensure_ascii=False)) - continue - - if options["test"]: - print('%s: valid %s subtitles' % (title, len(lines))) - else: - n = 0 - item.annotations.filter(layer="subtitles", languages=lang).delete() - for sub_en in subtitles_en.order_by('start'): - sub = Annotation() - sub.item = sub_en.item - sub.user = sub_en.user - sub.layer = sub_en.layer - sub.start = sub_en.start - sub.end = sub_en.end - sub.value = '%s' % (lang, lines[n]) - sub.save() - n += 1 - - ''' - srt = 'vocals_txt/%s/%s' % (title[0], title.replace('.wav', '.srt')) - filename = 'vocals_txt/%s/%s' % (title[0], title.replace('.wav', '.' + lang + '.srt')) - - folder = os.path.dirname(filename) - if not os.path.exists(folder): - os.makedirs(folder) - data = json.load(open(srt + '.json')) - subs = block.replace('\n\n', '\n').split('\n') - if len(data) != len(subs): - print('invalid', title, 'expected', len(data), 'got', len(subs)) - invalid.append('## %s\n\n%s' % (title, block)) - valid.append('## %s\n\n%s' % (title, '\n\n'.join([d['value'] for d in data]))) - continue - - for i, sub in enumerate(data): - sub['value'] = subs[i] - kodata = ox.srt.encode(data) - current = None - if os.path.exists(filename): - with open(filename, 'rb') as fd: - current = fd.read() - if current != kodata: - print('update', title, filename) - with open(filename, 'wb') as fd: - fd.write(kodata) - with open(filename + '.json', 'w') as fd: - ko = [{ - 'in': s['in'], - 'out': s['out'], - 'value': s['value'], - } for s in data] - json.dump(ko, fd, ensure_ascii=False, indent=4) - - if invalid: - with open('invalid_%s_subtitles.txt' % lang, 'w') as fd: - fd.write('\n\n\n\n'.join(invalid)) - with open('invalid_%s_subtitles_en.txt' % lang, 'w') as fd: - fd.write('\n\n\n\n'.join(valid)) - ''' diff --git a/management/commands/infinity.py b/management/commands/infinity.py index 0ca70fa..1dc85e3 100644 --- a/management/commands/infinity.py +++ b/management/commands/infinity.py @@ -13,19 +13,7 @@ class Command(BaseCommand): 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('--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') - parser.add_argument('--keep-audio', action='store_true', dest='keep_audio', default=False, help='keep independent audio tracks') - parser.add_argument('--stereo-downmix', action='store_true', dest='stereo_downmix', default=False, help='stereo downmix') - parser.add_argument('--debug', action='store_true', dest='debug', default=False, help='output more info') def handle(self, **options): - if options.get("config"): - if os.path.exists(options["config"]): - with open(options["config"]) as fd: - config = json.load(fd) - options.update(config) - else: - print("unable to load config %s" % options["config"]) render_infinity(options) diff --git a/management/commands/render.py b/management/commands/render.py index 91e8fa5..ace0d61 100644 --- a/management/commands/render.py +++ b/management/commands/render.py @@ -16,10 +16,6 @@ class Command(BaseCommand): parser.add_argument('--duration', action='store', dest='duration', default="3600", help='target duration of all fragments in seconds') parser.add_argument('--offset', action='store', dest='offset', default="1024", help='inital offset in pi') parser.add_argument('--no-video', action='store_true', dest='no_video', default=False, help='don\'t render video') - parser.add_argument('--single-file', action='store_true', dest='single_file', default=False, help='render to single video') - parser.add_argument('--keep-audio', action='store_true', dest='keep_audio', default=False, help='keep independent audio tracks') - parser.add_argument('--stereo-downmix', action='store_true', dest='stereo_downmix', default=False, help='stereo downmix') - parser.add_argument('--debug', action='store_true', dest='debug', default=False, help='output more info') def handle(self, **options): render_all(options) diff --git a/management/commands/update_subtitles.py b/management/commands/update_subtitles.py index 1585ff5..2ca2511 100644 --- a/management/commands/update_subtitles.py +++ b/management/commands/update_subtitles.py @@ -13,6 +13,7 @@ class Command(BaseCommand): 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('--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') def handle(self, **options): diff --git a/player/player-back.service b/player/player-back.service index b0a2f14..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/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u --config /srv/t_for_time/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 d514da8..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/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u --config /srv/t_for_time/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 28b41e0..74800e9 100755 --- a/player/player.py +++ b/player/player.py @@ -8,7 +8,6 @@ import time from threading import Thread from datetime import datetime -import ox import mpv @@ -20,17 +19,10 @@ SYNC_GRACE_TIME = 5 SYNC_JUMP_AHEAD = 1 PORT = 9067 DEBUG = False - -CONFIG = { - "font": "Menlo", - "font_size": 30, - "font_border": 4, - "sub_border_color": "0.0/0.0/0.0/0.75", - "sub_margin": 2 * 36 + 6, - "sub_spacing": 0, - "vf": None, - "sync_group": None, -} +FONT = 'Menlo' +FONT_SIZE = 30 +FONT_BORDER = 4 +SUB_MARGIN = 2 * 36 + 6 def hide_gnome_overview(): @@ -52,7 +44,6 @@ class Main: class Sync(Thread): active = True is_main = True - is_paused = False ready = False destination = "255.255.255.255" reload_check = None @@ -62,52 +53,32 @@ class Sync(Thread): def __init__(self, *args, **kwargs): self.is_main = kwargs.get('mode', 'main') == 'main' - self.start_at_hour = kwargs.get("hour", False) self.sock = self.init_socket() self.main = Main() if self.is_main: self.socket_enable_broadcast() - if kwargs.get("sax"): - self.sax = mpv.MPV( - log_handler=mpv_log, input_default_bindings=True, - input_vo_keyboard=True, - ) - self.sax.loop_file = True - self.sax.play("/srv/t_for_time/render/Saxophone-5.1.mp4") - else: - self.sax = None - if mpv.MPV_VERSION >= (2, 2): self.mpv = mpv.MPV( log_handler=mpv_log, input_default_bindings=True, input_vo_keyboard=True, - sub_font_size=CONFIG["font_size"], sub_font=CONFIG["font"], - sub_border_size=CONFIG["font_border"], - sub_border_color=CONFIG["sub_border_color"], - sub_margin_y=CONFIG["sub_margin"], - sub_ass_line_spacing=CONFIG["sub_spacing"], + sub_font_size=FONT_SIZE, sub_font=FONT, + sub_border_size=FONT_BORDER, + sub_margin_y=SUB_MARGIN, ) else: self.mpv = mpv.MPV( log_handler=mpv_log, input_default_bindings=True, input_vo_keyboard=True, - sub_text_font_size=CONFIG["font_size"], sub_text_font=CONFIG["font"], - sub_border_size=CONFIG["font_border"], - sub_border_color=CONFIG["sub_border_color"], - sub_margin_y=CONFIG["sub_margin"], - sub_ass_line_spacing=CONFIG["sub_spacing"], + sub_text_font_size=FONT_SIZE, sub_text_font=FONT, + sub_border_size=FONT_BORDER, + sub_margin_y=SUB_MARGIN, ) - if CONFIG.get("vf"): - self.mpv.vf = CONFIG["vf"] self.mpv.observe_property('time-pos', self.time_pos_cb) self.mpv.fullscreen = kwargs.get('fullscreen', False) self.mpv.loop_file = False self.mpv.loop_playlist = True self.mpv.register_key_binding('q', self.q_binding) - self.mpv.register_key_binding('s', self.s_binding) - self.mpv.register_key_binding('p', self.p_binding) - self.mpv.register_key_binding('SPACE', self.space_binding) self.playlist = kwargs['playlist'] self.playlist_mtime = os.stat(self.playlist).st_mtime self.mpv.loadlist(self.playlist) @@ -119,31 +90,6 @@ class Sync(Thread): time.sleep(0.1) self.mpv.pause = True self.sync_to_main() - elif self.start_at_hour: - self.mpv.pause = True - fmt = '%Y-%m-%d %H' - now = datetime.now() - offset = (now - datetime.strptime(now.strftime(fmt), fmt)).total_seconds() - if self.sax: - self.sax.wait_until_playing() - self.sax.seek(offset, 'absolute', 'exact') - self.sax.pause = True - position = 0 - for idx, item in enumerate(self.mpv.playlist): - duration = ox.avinfo(item['filename'])['duration'] - if position + duration > offset: - pos = offset - position - self.mpv.playlist_play_index(idx) - self.mpv.pause = False - self.mpv.wait_until_playing() - self.mpv.seek(pos, 'absolute', 'exact') - time.sleep(0.1) - break - else: - position += duration - if self.sax: - self.sax.pause = False - self.ready = True Thread.__init__(self) self.start() @@ -160,45 +106,16 @@ class Sync(Thread): else: self.read_position_main() self.reload_playlist() - if not self.is_paused and self._tick and abs(time.time() - self._tick) > 60: + if self._tick and abs(time.time() - self._tick) > 60: logger.error("player is stuck") self._tick = 0 self.stop() self.mpv.stop() def q_binding(self, *args): - if args[0] != 'd-': - return self.stop() self.mpv.stop() - def space_binding(self, *args): - if args[0] != 'd-': - return - if self.mpv.pause: - self.p_binding(*args) - else: - self.s_binding(*args) - - def s_binding(self, *args): - if args[0] != 'd-': - return - self.is_paused = True - self.mpv.pause = True - if self.sax: - self.sax.pause = True - self.send_playback_state() - - def p_binding(self, *args): - if args[0] != 'd-': - return - self.is_paused = False - self._tick = 0 - self.mpv.pause = False - if self.sax: - self.sax.pause = False - self.send_playback_state() - def stop(self, *args): self.active = False if self.sock: @@ -278,8 +195,6 @@ class Sync(Thread): "%0.4f %s" % (self.mpv.time_pos, self.mpv.playlist_current_pos) ).encode() - if CONFIG.get("sync_group"): - msg = ("%s " % CONFIG["sync_group"]).encode() + msg except: return try: @@ -287,47 +202,18 @@ class Sync(Thread): except socket.error as e: logger.error("send failed: %s", e) - def send_playback_state(self): - state = 'pause' if self.mpv.pause else 'play' - msg = ("%s -1" % state).encode() - try: - self.sock.send(msg) - except socket.error as e: - logger.error("send failed: %s", e) - # # follower specific # - _last_ping = None def read_position_main(self): self.sock.settimeout(5) - while True: - try: - data = self.sock.recvfrom(1024)[0].decode().split(" ", 1) - except socket.timeout: - if self._last_ping != "pause": - logger.error("failed to receive data from main") - return - except OSError: - logger.error("socket closed") - return - - if CONFIG.get("sync_group"): - if data[0] == str(CONFIG["sync_group"]): - data = data[1].split(" ", 1) - break - else: - break - - self._last_ping = data[0] - if data[0] == "pause": - self.is_paused = True - self.mpv.pause = True - elif data[0] == "play": - self.is_paused = False - self._tick = 0 - self.mpv.pause = False + try: + data = self.sock.recvfrom(1024)[0].decode().split(" ", 1) + except socket.timeout: + logger.error("failed to receive data from main") + except OSError: + logger.error("socket closed") else: self.main.time_pos = float(data[0]) self.main.playlist_current_pos = int(data[1]) @@ -416,26 +302,16 @@ def main(): 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) - parser.add_argument('--hour', action='store_true', help='hour', default=False) - parser.add_argument('--sax', action='store_true', help='hour', default=False) - parser.add_argument('--config', help='config', default=None) args = parser.parse_args() DEBUG = args.debug if DEBUG: log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' logging.basicConfig(level=logging.DEBUG, format=log_format) - if args.config: - if os.path.exists(args.config): - with open(args.config) as fd: - CONFIG.update(json.load(fd)) - else: - logger.error("config file %s does not exist, skipping", args.config) - base = os.path.dirname(os.path.abspath(__file__)) #os.chdir(base) - player = Sync(mode=args.mode, playlist=args.playlist, fullscreen=not args.window, hour=args.hour, sax=args.sax) + player = Sync(mode=args.mode, playlist=args.playlist, fullscreen=not args.window) while player.active: try: player.mpv.wait_for_playback() diff --git a/render.py b/render.py index e62f513..0be09fd 100644 --- a/render.py +++ b/render.py @@ -11,10 +11,8 @@ import time from pathlib import Path import ox -import lxml.etree - from .pi import random -from .render_kdenlive import KDEnliveProject, _CACHE, melt_xml, get_melt +from .render_kdenlive import KDEnliveProject, _CACHE def random_int(seq, length): @@ -61,18 +59,13 @@ 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) -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 +def compose(clips, target=150, base=1024, voice_over=None): length = 0 scene = { 'front': { @@ -107,7 +100,6 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): used = [] voice_overs = [] - sub_offset = 0 if voice_over: vo_keys = list(sorted(voice_over)) if chance(seq, 0.5): @@ -126,7 +118,7 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): if vo_min > target: target = vo_min elif vo_min < target: - offset = format_duration((target - vo_min) / 2, fps) + offset = (target - vo_min) / 2 scene['audio-center']['A1'].append({ 'blank': True, 'duration': offset @@ -140,29 +132,17 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): subs = [] for vo in voice_overs: voc = vo.copy() - a, b = '-11', '-3' + a, b = '3', '-6' if 'Whispered' in voc['src']: - a, b = '-8', '0' + a, b = '6', '-3' elif 'Read' in voc['src']: - a, b = '-7.75', '0.25' + a, b = '6.25', '-2.75' elif 'Free' in voc['src']: - a, b = '-8.8', '-0.8' + a, b = '5.2', '-3.8' elif 'Ashley' in voc['src']: - a, b = '-9.5', '-1.50' + a, b = '3.75', '-5.25' 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' + a, b = '4.25', '-4.75' voc['filter'] = {'volume': a} scene['audio-center']['A1'].append(voc) vo_low = vo.copy() @@ -206,7 +186,7 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): 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 + length += clip['duration'] if "foreground" not in clip and "animation" in clip: fg = clip['animation'] @@ -299,16 +279,13 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): 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}, + 'filter': {'volume': '+0.2'}, }) # TBD: Foley - cf_volume = '-2.5' + cf_volume = '-5.5' scene['audio-front']['A2'].append({ 'duration': clip['duration'], 'src': foley, @@ -321,83 +298,26 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): }) 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 + duration += clip['duration'] + return duration -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 = {} +def render(root, scene, prefix=''): fps = 24 files = [] - scene_duration = int(get_scene_duration(scene) * fps) + scene_duration = int(get_scene_duration(scene) * 24) 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) @@ -408,43 +328,21 @@ def render(root, scene, prefix='', options=None): #print(track) for clip in clips: project.append_clip(track, clip) - track_durations[track] = sum([int(c['duration'] * fps) for c in clips]) + track_durations[track] = int(sum([c['duration'] for c in clips]) * 24) 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}) - + project.append_clip(track, {'blank': True, "duration": delta/24}) + break 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 @@ -485,32 +383,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']) @@ -548,13 +429,7 @@ def render_all(options): 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 - ) + scene, used = compose(unused_fragment_clips, target=target, base=fragment_base, voice_over=fragment['voice_over']) clips_used += used scene_duration = get_scene_duration(scene) print("%s %6.3f -> %6.3f (%6.3f)" % (name, target, scene_duration, fragment_target)) @@ -569,19 +444,20 @@ def render_all(options): elif position < target_position: target = target + 0.1 * fragment_target - timelines = render(prefix, scene, fragment_prefix[len(prefix) + 1:] + '/', options) + timelines = render(prefix, scene, fragment_prefix[len(prefix) + 1:] + '/') 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"]: + if not options['no_video']: for timeline in timelines: print(timeline) ext = '.mp4' if '/audio' in timeline: ext = '.wav' - cmd = get_melt() + [ - timeline, + cmd = [ + 'xvfb-run', '-a', + 'melt', timeline, '-quiet', '-consumer', 'avformat:%s' % timeline.replace('.kdenlive', ext), ] @@ -603,8 +479,8 @@ def render_all(options): subprocess.call(cmd) os.unlink(timeline.replace('.kdenlive', ext)) - cmds = [] fragment_prefix = Path(fragment_prefix) + cmds = [] for src, out1, out2 in ( ("audio-front.wav", "fl.wav", "fr.wav"), ("audio-center.wav", "fc.wav", "lfe.wav"), @@ -631,51 +507,29 @@ def render_all(options): "-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", + "-i", fragment_prefix / "audio-5.1.mp4", + "-c", "copy", + fragment_prefix / "front-5.1.mp4", ]) cmds.append([ "ffmpeg", "-y", "-nostats", "-loglevel", "error", "-i", fragment_prefix / "back.mp4", - "-i", fragment_prefix / audio_back, + "-i", fragment_prefix / "audio-back.wav", "-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])) + #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"), + ("front-5.1.mp4", "back.mp4"), ): duration_a = ox.avinfo(str(fragment_prefix / a))['duration'] duration_b = ox.avinfo(str(fragment_prefix / b))['duration'] @@ -684,178 +538,26 @@ def render_all(options): 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") + shutil.move(fragment_prefix / "front-5.1.mp4", fragment_prefix / "front.mp4") for fn in ( "audio-5.1.mp4", - "audio-center.wav", "audio-rear.wav", + "audio-center.wav", "audio-rear.wav", "audio-center.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): +def get_srt(sub, offset=0): 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) + sdata['value'] = sdata['value'].replace('
', '
').replace('
\n', '\n').replace('
', '\n') if offset: sdata["in"] += offset sdata["out"] += offset @@ -872,56 +574,12 @@ def fix_overlaps(data): previous = sub return data -def shift_clips(data, offset): - for clip in data: - clip['in'] += offset - clip['out'] += offset - return data - -def scene_subtitles(scene, options): - import item.models - offset = 0 - 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']) + duration = int(options['duration']) base = int(options['offset']) - lang, tlang = parse_lang(options["lang"]) _cache = os.path.join(prefix, "cache.json") if os.path.exists(_cache): @@ -931,64 +589,27 @@ def update_subtitles(options): 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: + with open(folder / "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").exclude(value="").order_by("start"): + sdata = get_srt(sub, offset) + 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")) @@ -1015,24 +636,19 @@ 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 = { + "offset": 100, + "max-items": 30, + "no_video": False, + } + for key in ("prefix", "duration"): 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 = [ @@ -1040,8 +656,8 @@ def render_infinity(options): 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"]] + current = list(reversed(ox.sorted_strings(current))) + remove = list(reversed(current[-state["max-items"]:])) update_m3u(render_prefix, exclude=remove) for folder in remove: folder = render_prefix + folder @@ -1059,157 +675,3 @@ def render_infinity(options): with open(state_f + "~", "w") as fd: json.dump(state, fd, indent=2) shutil.move(state_f + "~", state_f) - - -def join_subtitles(base_prefix, 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_json in scenes: - with open(scene_json) as fd: - scene = json.load(fd) - 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/render_kdenlive.py b/render_kdenlive.py index cdf755b..147fbef 100644 --- a/render_kdenlive.py +++ b/render_kdenlive.py @@ -4,7 +4,6 @@ import subprocess import lxml.etree import uuid import os -import sys _CACHE = {} _IDS = defaultdict(int) @@ -13,14 +12,6 @@ def get_propery(element, name): return element.xpath('property[@name="%s"]' % name)[0].text -def get_melt(): - cmd = ['melt'] - if 'XDG_RUNTIME_DIR' not in os.environ: - os.environ['XDG_RUNTIME_DIR'] = '/tmp/runtime-pandora' - if 'DISPLAY' not in os.environ: - cmd = ['xvfb-run', '-a'] + cmd - return cmd - def melt_xml(file): out = None real_path = os.path.realpath(file) @@ -29,8 +20,7 @@ def melt_xml(file): if os.stat(real_path).st_mtime != ts: out = None if not out: - cmd = get_melt() + [file, '-consumer', 'xml'] - out = subprocess.check_output(cmd).decode() + out = subprocess.check_output(['melt', file, '-consumer', 'xml']).decode() _CACHE[file] = [os.stat(real_path).st_mtime, out] return out @@ -564,6 +554,7 @@ class KDEnliveProject: ] + value) ] + def properties(self, *props): return [ self.get_element("property", attrib={"name": name}, text=str(value) if value is not None else value) diff --git a/sax.py b/sax.py index c1e916b..3009ea6 100644 --- a/sax.py +++ b/sax.py @@ -31,7 +31,7 @@ reverb = { "src": reverb_wav, "duration": 3600.0, "filter": { - "volume": "3.5" + "volume": "0.5" }, } @@ -39,14 +39,14 @@ long = { "src": long_wav, "duration": 3600.0, "filter": { - "volume": "-1" + "volume": "-4" }, } noise = { "src": nois_wav, "duration": 3600.0, "filter": { - "volume": "7.75" + "volume": "4.75" }, } diff --git a/title.png b/title.png deleted file mode 100644 index c92b10e..0000000 Binary files a/title.png and /dev/null differ