Compare commits

..

26 commits

Author SHA1 Message Date
j
f7dfa96389 fix infinity 2025-02-12 17:50:53 +01:00
j
b2552d6059 make sure all tracks are exactly the same length 2024-12-04 09:16:24 +00:00
j
95a41fc2e2 single video render 2024-12-03 20:12:15 +00:00
j
2a2516bff9 pad audio tracks to scene duration 2024-12-03 19:35:37 +00:00
j
c8991438bb group fixes 2024-09-10 13:03:49 +01:00
j
45e2acbbb8 avoid recursion 2024-09-10 12:46:47 +01:00
j
e221626191 sync group 2024-09-10 12:42:16 +01:00
j
9021131e8d newline 2024-08-29 17:36:30 +02:00
j
44bd62897c avoid double 2024-08-29 17:36:18 +02:00
j
793da444ad fix subtitle import 2024-04-30 16:44:35 +01:00
j
4f57230996 increase the melody version of Bani's singing by 0.5 db 2024-04-14 10:36:13 +01:00
j
1ac5574bfc reset tick 2024-04-04 23:37:29 +01:00
j
3c9f200fdd don't reset player if its paused 2024-04-04 23:24:56 +01:00
j
569c72ee8b skip folders without scene.json 2024-04-02 12:37:17 +02:00
j
7654fc7d6c sub border color 2024-04-02 12:35:01 +02:00
j
f8bb75cd5b duration not needed for subtitle updates 2024-04-02 11:30:46 +02:00
j
8268166b77 load player config from file 2024-04-01 12:08:21 +02:00
j
6cdbf4f1b9 forward pause/play to peers 2024-03-23 10:00:47 +01:00
j
a6479d1746 fix seek in sax 2024-03-22 14:51:08 +01:00
j
19b54d57cb include sound adjustments
Front L & R: +3.0
Centre: -14.0
Rear: L & R +3.0
Wall (Back): L & R -8.0 (These is the stereo pair attached to the "original" clips)
2024-03-22 14:25:13 +01:00
j
d72bf343e3 adjust levels
- Melody (Ban's vocals) (+ 1 db)
- Ashley (Ban's vocals) (- 0.75 db)
2024-03-22 14:13:16 +01:00
j
ed03c7026a sync to hour, play sax inline, add s/p keybindings 2024-03-22 14:10:07 +01:00
j
438108a8f9 fix update_subtitles 2024-03-22 12:29:11 +01:00
j
3782ca6721 multiple languages 2024-03-22 11:33:39 +01:00
j
80db2f0255 import/export subtitles 2024-03-22 10:56:50 +01:00
j
01f669b61d better way to calculate remove 2024-03-19 11:48:36 +01:00
11 changed files with 562 additions and 67 deletions

View file

@ -2,24 +2,43 @@ import json
import os import os
import subprocess import subprocess
import ox
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
from ...render import add_translations
class Command(BaseCommand): class Command(BaseCommand):
help = 'export all subtitles for translations' help = 'export all subtitles for translations'
def add_arguments(self, parser): def add_arguments(self, parser):
pass parser.add_argument('--lang', action='store', dest='lang', default=None, help='subtitle language')
def handle(self, **options): def handle(self, **options):
import annotation.models import annotation.models
import item.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'): 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)) print("## %s %s" % (i.get("title"), i.public_id))
for sub in i.annotations.all().filter(layer='subtitles').exclude(value='').order_by("start"): for sub in i.annotations.all().filter(layer='subtitles').exclude(value='').filter(languages=lang).order_by("start"):
if not sub.languages: if tlang:
print(sub.value.strip() + "\n") value = add_translations(sub, tlang)
value = ox.strip_tags(value)
else:
value = sub.value.replace('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\n').strip()
if sub.languages:
value = ox.strip_tags(value)
print(value.strip() + "\n")
print("\n\n\n") print("\n\n\n")

View file

@ -23,15 +23,28 @@ def resolve_roman(s):
return s.replace(extra, new) return s.replace(extra, new)
return s return s
def format_duration(duration, fps):
return float('%0.5f' % (round(duration * fps) / fps))
class Command(BaseCommand): class Command(BaseCommand):
help = 'generate symlinks to clips and clips.json' help = 'generate symlinks to clips and clips.json'
def add_arguments(self, parser): 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('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in')
def handle(self, **options): def handle(self, **options):
prefix = options['prefix'] 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
clips = [] clips = []
for i in item.models.Item.objects.filter(sort__type='original'): 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) qs = item.models.Item.objects.filter(data__title=i.data['title']).exclude(id=i.id)
@ -58,6 +71,10 @@ class Command(BaseCommand):
if not clip["duration"]: if not clip["duration"]:
print('!!', durations, clip) print('!!', durations, clip)
continue 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['tags'] = i.data.get('tags', [])
clip['editingtags'] = i.data.get('editingtags', []) clip['editingtags'] = i.data.get('editingtags', [])
name = os.path.basename(clip['original']) name = os.path.basename(clip['original'])
@ -102,12 +119,12 @@ class Command(BaseCommand):
os.unlink(target) os.unlink(target)
os.symlink(src, target) os.symlink(src, target)
subs = [] subs = []
for sub in vo.annotations.filter(layer="subtitles").exclude(value="").order_by("start"): for sub in vo.annotations.filter(layer="subtitles", languages=lang).exclude(value="").order_by("start"):
sdata = get_srt(sub) sdata = get_srt(sub, lang=tlang)
subs.append(sdata) subs.append(sdata)
voice_over[fragment_id][batch] = { voice_over[fragment_id][batch] = {
"src": target, "src": target,
"duration": source.duration, "duration": format_duration(source.duration, 24),
"subs": subs "subs": subs
} }
with open(os.path.join(prefix, 'voice_over.json'), 'w') as fd: with open(os.path.join(prefix, 'voice_over.json'), 'w') as fd:

View file

@ -0,0 +1,109 @@
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 = '<span lang="%s">%s</span>' % (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))
'''

View file

@ -14,6 +14,9 @@ class Command(BaseCommand):
def add_arguments(self, parser): 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="/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('--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('--debug', action='store_true', dest='debug', default=False, help='output more info')
def handle(self, **options): def handle(self, **options):
render_infinity(options) render_infinity(options)

View file

@ -16,6 +16,9 @@ class Command(BaseCommand):
parser.add_argument('--duration', action='store', dest='duration', default="3600", help='target duration of all fragments in seconds') 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('--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('--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('--debug', action='store_true', dest='debug', default=False, help='output more info')
def handle(self, **options): def handle(self, **options):
render_all(options) render_all(options)

View file

@ -13,8 +13,8 @@ class Command(BaseCommand):
def add_arguments(self, parser): 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="/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('--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): def handle(self, **options):
update_subtitles(options) update_subtitles(options)

View file

@ -8,6 +8,7 @@ import time
from threading import Thread from threading import Thread
from datetime import datetime from datetime import datetime
import ox
import mpv import mpv
@ -19,10 +20,17 @@ SYNC_GRACE_TIME = 5
SYNC_JUMP_AHEAD = 1 SYNC_JUMP_AHEAD = 1
PORT = 9067 PORT = 9067
DEBUG = False DEBUG = False
FONT = 'Menlo'
FONT_SIZE = 30 CONFIG = {
FONT_BORDER = 4 "font": "Menlo",
SUB_MARGIN = 2 * 36 + 6 "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,
}
def hide_gnome_overview(): def hide_gnome_overview():
@ -44,6 +52,7 @@ class Main:
class Sync(Thread): class Sync(Thread):
active = True active = True
is_main = True is_main = True
is_paused = False
ready = False ready = False
destination = "255.255.255.255" destination = "255.255.255.255"
reload_check = None reload_check = None
@ -53,32 +62,52 @@ class Sync(Thread):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.is_main = kwargs.get('mode', 'main') == 'main' self.is_main = kwargs.get('mode', 'main') == 'main'
self.start_at_hour = kwargs.get("hour", False)
self.sock = self.init_socket() self.sock = self.init_socket()
self.main = Main() self.main = Main()
if self.is_main: if self.is_main:
self.socket_enable_broadcast() 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): if mpv.MPV_VERSION >= (2, 2):
self.mpv = mpv.MPV( self.mpv = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True, log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True, input_vo_keyboard=True,
sub_font_size=FONT_SIZE, sub_font=FONT, sub_font_size=CONFIG["font_size"], sub_font=CONFIG["font"],
sub_border_size=FONT_BORDER, sub_border_size=CONFIG["font_border"],
sub_margin_y=SUB_MARGIN, sub_border_color=CONFIG["sub_border_color"],
sub_margin_y=CONFIG["sub_margin"],
sub_ass_line_spacing=CONFIG["sub_spacing"],
) )
else: else:
self.mpv = mpv.MPV( self.mpv = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True, log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True, input_vo_keyboard=True,
sub_text_font_size=FONT_SIZE, sub_text_font=FONT, sub_text_font_size=CONFIG["font_size"], sub_text_font=CONFIG["font"],
sub_border_size=FONT_BORDER, sub_border_size=CONFIG["font_border"],
sub_margin_y=SUB_MARGIN, sub_border_color=CONFIG["sub_border_color"],
sub_margin_y=CONFIG["sub_margin"],
sub_ass_line_spacing=CONFIG["sub_spacing"],
) )
if CONFIG.get("vf"):
self.mpv.vf = CONFIG["vf"]
self.mpv.observe_property('time-pos', self.time_pos_cb) self.mpv.observe_property('time-pos', self.time_pos_cb)
self.mpv.fullscreen = kwargs.get('fullscreen', False) self.mpv.fullscreen = kwargs.get('fullscreen', False)
self.mpv.loop_file = False self.mpv.loop_file = False
self.mpv.loop_playlist = True self.mpv.loop_playlist = True
self.mpv.register_key_binding('q', self.q_binding) 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 = kwargs['playlist']
self.playlist_mtime = os.stat(self.playlist).st_mtime self.playlist_mtime = os.stat(self.playlist).st_mtime
self.mpv.loadlist(self.playlist) self.mpv.loadlist(self.playlist)
@ -90,6 +119,31 @@ class Sync(Thread):
time.sleep(0.1) time.sleep(0.1)
self.mpv.pause = True self.mpv.pause = True
self.sync_to_main() 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 self.ready = True
Thread.__init__(self) Thread.__init__(self)
self.start() self.start()
@ -106,16 +160,45 @@ class Sync(Thread):
else: else:
self.read_position_main() self.read_position_main()
self.reload_playlist() self.reload_playlist()
if self._tick and abs(time.time() - self._tick) > 60: if not self.is_paused and self._tick and abs(time.time() - self._tick) > 60:
logger.error("player is stuck") logger.error("player is stuck")
self._tick = 0 self._tick = 0
self.stop() self.stop()
self.mpv.stop() self.mpv.stop()
def q_binding(self, *args): def q_binding(self, *args):
if args[0] != 'd-':
return
self.stop() self.stop()
self.mpv.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): def stop(self, *args):
self.active = False self.active = False
if self.sock: if self.sock:
@ -195,6 +278,8 @@ class Sync(Thread):
"%0.4f %s" "%0.4f %s"
% (self.mpv.time_pos, self.mpv.playlist_current_pos) % (self.mpv.time_pos, self.mpv.playlist_current_pos)
).encode() ).encode()
if CONFIG.get("sync_group"):
msg = ("%s " % CONFIG["sync_group"]).encode() + msg
except: except:
return return
try: try:
@ -202,18 +287,47 @@ class Sync(Thread):
except socket.error as e: except socket.error as e:
logger.error("send failed: %s", 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 # follower specific
# #
_last_ping = None
def read_position_main(self): def read_position_main(self):
self.sock.settimeout(5) self.sock.settimeout(5)
try: while True:
data = self.sock.recvfrom(1024)[0].decode().split(" ", 1) try:
except socket.timeout: data = self.sock.recvfrom(1024)[0].decode().split(" ", 1)
logger.error("failed to receive data from main") except socket.timeout:
except OSError: if self._last_ping != "pause":
logger.error("socket closed") 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
else: else:
self.main.time_pos = float(data[0]) self.main.time_pos = float(data[0])
self.main.playlist_current_pos = int(data[1]) self.main.playlist_current_pos = int(data[1])
@ -302,16 +416,23 @@ def main():
parser.add_argument('--prefix', help='video location', default=prefix) 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('--window', action='store_true', help='run in window', default=False)
parser.add_argument('--debug', action='store_true', help='debug', 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() args = parser.parse_args()
DEBUG = args.debug DEBUG = args.debug
if DEBUG: if DEBUG:
log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
logging.basicConfig(level=logging.DEBUG, format=log_format) logging.basicConfig(level=logging.DEBUG, format=log_format)
if args.config:
with open(args.config) as fd:
CONFIG.update(json.load(fd))
base = os.path.dirname(os.path.abspath(__file__)) base = os.path.dirname(os.path.abspath(__file__))
#os.chdir(base) #os.chdir(base)
player = Sync(mode=args.mode, playlist=args.playlist, fullscreen=not args.window) player = Sync(mode=args.mode, playlist=args.playlist, fullscreen=not args.window, hour=args.hour, sax=args.sax)
while player.active: while player.active:
try: try:
player.mpv.wait_for_playback() player.mpv.wait_for_playback()

284
render.py
View file

@ -11,8 +11,10 @@ import time
from pathlib import Path from pathlib import Path
import ox import ox
import lxml.etree
from .pi import random from .pi import random
from .render_kdenlive import KDEnliveProject, _CACHE from .render_kdenlive import KDEnliveProject, _CACHE, melt_xml, get_melt
def random_int(seq, length): def random_int(seq, length):
@ -64,8 +66,11 @@ def write_if_new(path, data, mode=''):
with open(path, write_mode) as fd: with open(path, write_mode) as fd:
fd.write(data) 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): def compose(clips, target=150, base=1024, voice_over=None):
fps = 24
length = 0 length = 0
scene = { scene = {
'front': { 'front': {
@ -100,6 +105,7 @@ def compose(clips, target=150, base=1024, voice_over=None):
used = [] used = []
voice_overs = [] voice_overs = []
sub_offset = 0
if voice_over: if voice_over:
vo_keys = list(sorted(voice_over)) vo_keys = list(sorted(voice_over))
if chance(seq, 0.5): if chance(seq, 0.5):
@ -118,7 +124,7 @@ def compose(clips, target=150, base=1024, voice_over=None):
if vo_min > target: if vo_min > target:
target = vo_min target = vo_min
elif vo_min < target: elif vo_min < target:
offset = (target - vo_min) / 2 offset = format_duration((target - vo_min) / 2, fps)
scene['audio-center']['A1'].append({ scene['audio-center']['A1'].append({
'blank': True, 'blank': True,
'duration': offset 'duration': offset
@ -132,17 +138,17 @@ def compose(clips, target=150, base=1024, voice_over=None):
subs = [] subs = []
for vo in voice_overs: for vo in voice_overs:
voc = vo.copy() voc = vo.copy()
a, b = '3', '-6' a, b = '-11', '-3'
if 'Whispered' in voc['src']: if 'Whispered' in voc['src']:
a, b = '6', '-3' a, b = '-8', '0'
elif 'Read' in voc['src']: elif 'Read' in voc['src']:
a, b = '6.25', '-2.75' a, b = '-7.75', '0.25'
elif 'Free' in voc['src']: elif 'Free' in voc['src']:
a, b = '5.2', '-3.8' a, b = '-8.8', '-0.8'
elif 'Ashley' in voc['src']: elif 'Ashley' in voc['src']:
a, b = '3.75', '-5.25' a, b = '-9.5', '-1.50'
elif 'Melody' in voc['src']: elif 'Melody' in voc['src']:
a, b = '4.25', '-4.75' a, b = '-5.25', '-0.25'
voc['filter'] = {'volume': a} voc['filter'] = {'volume': a}
scene['audio-center']['A1'].append(voc) scene['audio-center']['A1'].append(voc)
vo_low = vo.copy() vo_low = vo.copy()
@ -186,7 +192,7 @@ def compose(clips, target=150, base=1024, voice_over=None):
if length + clip['duration'] > target and length >= vo_min: if length + clip['duration'] > target and length >= vo_min:
break break
print('%06.3f %06.3f' % (length, clip['duration']), os.path.basename(clip['original'])) print('%06.3f %06.3f' % (length, clip['duration']), os.path.basename(clip['original']))
length += clip['duration'] length += int(clip['duration'] * fps) / fps
if "foreground" not in clip and "animation" in clip: if "foreground" not in clip and "animation" in clip:
fg = clip['animation'] fg = clip['animation']
@ -282,10 +288,10 @@ def compose(clips, target=150, base=1024, voice_over=None):
scene['audio-back']['A1'].append({ scene['audio-back']['A1'].append({
'duration': clip['duration'], 'duration': clip['duration'],
'src': clip['original'], 'src': clip['original'],
'filter': {'volume': '+0.2'}, 'filter': {'volume': '-8.2'},
}) })
# TBD: Foley # TBD: Foley
cf_volume = '-5.5' cf_volume = '-2.5'
scene['audio-front']['A2'].append({ scene['audio-front']['A2'].append({
'duration': clip['duration'], 'duration': clip['duration'],
'src': foley, 'src': foley,
@ -298,20 +304,61 @@ def compose(clips, target=150, base=1024, voice_over=None):
}) })
used.append(clip) used.append(clip)
print("scene duration %0.3f (target: %0.3f, vo_min: %0.3f)" % (length, target, vo_min)) 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 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): def get_scene_duration(scene):
if isinstance(scene, str):
with open(scene) as fd:
scene = json.load(fd)
duration = 0 duration = 0
for key, value in scene.items(): for key, value in scene.items():
for name, clips in value.items(): for name, clips in value.items():
for clip in clips: for clip in clips:
duration += clip['duration'] duration += int(clip['duration'] * 24)
return duration return duration / 24
def render(root, scene, prefix=''): 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 render(root, scene, prefix='', options=None):
if options is None: options = {}
fps = 24 fps = 24
files = [] files = []
scene_duration = int(get_scene_duration(scene) * 24) scene_duration = int(get_scene_duration(scene) * fps)
for timeline, data in scene.items(): for timeline, data in scene.items():
if timeline == "subtitles": if timeline == "subtitles":
path = os.path.join(root, prefix + "front.srt") path = os.path.join(root, prefix + "front.srt")
@ -328,21 +375,43 @@ def render(root, scene, prefix=''):
#print(track) #print(track)
for clip in clips: for clip in clips:
project.append_clip(track, clip) project.append_clip(track, clip)
track_durations[track] = int(sum([c['duration'] for c in clips]) * 24) track_durations[track] = sum([int(c['duration'] * fps) for c in clips])
if timeline.startswith('audio-'): if timeline.startswith('audio-'):
track_duration = project.get_duration() track_duration = project.get_duration()
delta = scene_duration - track_duration delta = scene_duration - track_duration
if delta > 0: if delta > 0:
for track in track_durations: for track in track_durations:
if track_durations[track] == track_duration: if track_durations[track] == track_duration:
project.append_clip(track, {'blank': True, "duration": delta/24}) project.append_clip(track, {'blank': True, "duration": delta/fps})
break
path = os.path.join(root, prefix + "%s.kdenlive" % timeline) path = os.path.join(root, prefix + "%s.kdenlive" % timeline)
project_xml = project.to_xml() project_xml = project.to_xml()
write_if_new(path, project_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) files.append(path)
return files 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): def get_fragments(clips, voice_over, prefix):
import itemlist.models import itemlist.models
import item.models import item.models
@ -444,20 +513,19 @@ def render_all(options):
elif position < target_position: elif position < target_position:
target = target + 0.1 * fragment_target target = target + 0.1 * fragment_target
timelines = render(prefix, scene, fragment_prefix[len(prefix) + 1:] + '/') timelines = render(prefix, scene, fragment_prefix[len(prefix) + 1:] + '/', options)
scene_json = json.dumps(scene, indent=2, ensure_ascii=False) scene_json = json.dumps(scene, indent=2, ensure_ascii=False)
write_if_new(os.path.join(fragment_prefix, 'scene.json'), scene_json) write_if_new(os.path.join(fragment_prefix, 'scene.json'), scene_json)
if not options['no_video']: if not options['no_video'] and not options["single_file"]:
for timeline in timelines: for timeline in timelines:
print(timeline) print(timeline)
ext = '.mp4' ext = '.mp4'
if '/audio' in timeline: if '/audio' in timeline:
ext = '.wav' ext = '.wav'
cmd = [ cmd = get_melt() + [
'xvfb-run', '-a', timeline,
'melt', timeline,
'-quiet', '-quiet',
'-consumer', 'avformat:%s' % timeline.replace('.kdenlive', ext), '-consumer', 'avformat:%s' % timeline.replace('.kdenlive', ext),
] ]
@ -479,8 +547,8 @@ def render_all(options):
subprocess.call(cmd) subprocess.call(cmd)
os.unlink(timeline.replace('.kdenlive', ext)) os.unlink(timeline.replace('.kdenlive', ext))
fragment_prefix = Path(fragment_prefix)
cmds = [] cmds = []
fragment_prefix = Path(fragment_prefix)
for src, out1, out2 in ( for src, out1, out2 in (
("audio-front.wav", "fl.wav", "fr.wav"), ("audio-front.wav", "fl.wav", "fr.wav"),
("audio-center.wav", "fc.wav", "lfe.wav"), ("audio-center.wav", "fc.wav", "lfe.wav"),
@ -524,7 +592,8 @@ def render_all(options):
fragment_prefix / "back-audio.mp4", fragment_prefix / "back-audio.mp4",
]) ])
for cmd in cmds: for cmd in cmds:
#print(" ".join([str(x) for x in cmd])) if options["debug"]:
print(" ".join([str(x) for x in cmd]))
subprocess.call(cmd) subprocess.call(cmd)
for a, b in ( for a, b in (
@ -539,9 +608,13 @@ def render_all(options):
sys.exit(-1) sys.exit(-1)
shutil.move(fragment_prefix / "back-audio.mp4", fragment_prefix / "back.mp4") shutil.move(fragment_prefix / "back-audio.mp4", fragment_prefix / "back.mp4")
shutil.move(fragment_prefix / "front-5.1.mp4", fragment_prefix / "front.mp4") shutil.move(fragment_prefix / "front-5.1.mp4", fragment_prefix / "front.mp4")
if options["keep_audio"]:
shutil.move(fragment_prefix / "audio-center.wav", fragment_prefix / "vocals.wav")
shutil.move(fragment_prefix / "audio-front.wav", fragment_prefix / "foley.wav")
shutil.move(fragment_prefix / "audio-back.wav", fragment_prefix / "original.wav")
for fn in ( for fn in (
"audio-5.1.mp4", "audio-5.1.mp4",
"audio-center.wav", "audio-rear.wav", "audio-center.wav", "audio-center.wav", "audio-rear.wav",
"audio-front.wav", "audio-back.wav", "back-audio.mp4", "audio-front.wav", "audio-back.wav", "back-audio.mp4",
"fl.wav", "fr.wav", "fc.wav", "lfe.wav", "bl.wav", "br.wav", "fl.wav", "fr.wav", "fc.wav", "lfe.wav", "bl.wav", "br.wav",
): ):
@ -549,15 +622,132 @@ def render_all(options):
if os.path.exists(fn): if os.path.exists(fn):
os.unlink(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",
base_prefix / "front-5.1.mp4",
])
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-i", base_prefix / "back.mp4",
"-i", base_prefix / "audio-back.wav",
"-c:v", "copy",
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-5.1.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-5.1.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)
print("Duration - Target: %s Actual: %s" % (target_position, position)) print("Duration - Target: %s Actual: %s" % (target_position, position))
print(json.dumps(dict(stats), sort_keys=True, indent=2)) print(json.dumps(dict(stats), sort_keys=True, indent=2))
with open(_cache, "w") as fd: with open(_cache, "w") as fd:
json.dump(_CACHE, fd) json.dump(_CACHE, fd)
def get_srt(sub, offset=0): def add_translations(sub, lang):
value = sub.value.replace('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\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('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\n').strip()
if tsub.languages:
tvalue = ox.strip_tags(tvalue)
value += '\n' + tvalue
return value
def get_srt(sub, offset=0, lang=None):
sdata = sub.json(keys=['in', 'out', 'value']) sdata = sub.json(keys=['in', 'out', 'value'])
sdata['value'] = sdata['value'].replace('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\n') sdata['value'] = sdata['value'].replace('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\n').strip()
if lang:
sdata['value'] = add_translations(sub, lang)
if offset: if offset:
sdata["in"] += offset sdata["in"] += offset
sdata["out"] += offset sdata["out"] += offset
@ -578,8 +768,17 @@ def update_subtitles(options):
import item.models import item.models
prefix = Path(options['prefix']) prefix = Path(options['prefix'])
duration = int(options['duration'])
base = int(options['offset']) 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
_cache = os.path.join(prefix, "cache.json") _cache = os.path.join(prefix, "cache.json")
if os.path.exists(_cache): if os.path.exists(_cache):
@ -589,7 +788,10 @@ def update_subtitles(options):
base_prefix = prefix / 'render' / str(base) base_prefix = prefix / 'render' / str(base)
for folder in os.listdir(base_prefix): for folder in os.listdir(base_prefix):
folder = base_prefix / folder folder = base_prefix / folder
with open(folder / "scene.json") as fd: scene_json = folder / "scene.json"
if not os.path.exists(scene_json):
continue
with open(scene_json) as fd:
scene = json.load(fd) scene = json.load(fd)
offset = 0 offset = 0
subs = [] subs = []
@ -599,8 +801,8 @@ def update_subtitles(options):
vo = item.models.Item.objects.filter(data__batch__icontains=batch, data__title__startswith=fragment_id + '_').first() vo = item.models.Item.objects.filter(data__batch__icontains=batch, data__title__startswith=fragment_id + '_').first()
if vo: if vo:
#print("%s => %s %s" % (clip['src'], vo, vo.get('batch'))) #print("%s => %s %s" % (clip['src'], vo, vo.get('batch')))
for sub in vo.annotations.filter(layer="subtitles").exclude(value="").order_by("start"): for sub in vo.annotations.filter(layer="subtitles").filter(languages=lang).exclude(value="").order_by("start"):
sdata = get_srt(sub, offset) sdata = get_srt(sub, offset, tlang)
subs.append(sdata) subs.append(sdata)
else: else:
print("could not find vo for %s" % clip['src']) print("could not find vo for %s" % clip['src'])
@ -646,7 +848,7 @@ def render_infinity(options):
"max-items": 30, "max-items": 30,
"no_video": False, "no_video": False,
} }
for key in ("prefix", "duration"): for key in ("prefix", "duration", "debug", "single_file", "keep_audio"):
state[key] = options[key] state[key] = options[key]
while True: while True:
@ -656,8 +858,8 @@ def render_infinity(options):
if f.isdigit() and os.path.isdir(render_prefix + f) and state["offset"] > int(f) >= 100 if f.isdigit() and os.path.isdir(render_prefix + f) and state["offset"] > int(f) >= 100
] ]
if len(current) > state["max-items"]: if len(current) > state["max-items"]:
current = list(reversed(ox.sorted_strings(current))) current = ox.sorted_strings(current)
remove = list(reversed(current[-state["max-items"]:])) remove = current[:-state["max-items"]]
update_m3u(render_prefix, exclude=remove) update_m3u(render_prefix, exclude=remove)
for folder in remove: for folder in remove:
folder = render_prefix + folder folder = render_prefix + folder
@ -675,3 +877,15 @@ def render_infinity(options):
with open(state_f + "~", "w") as fd: with open(state_f + "~", "w") as fd:
json.dump(state, fd, indent=2) json.dump(state, fd, indent=2)
shutil.move(state_f + "~", state_f) shutil.move(state_f + "~", state_f)
def join_subtitles(base_prefix):
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))

View file

@ -4,6 +4,7 @@ import subprocess
import lxml.etree import lxml.etree
import uuid import uuid
import os import os
import sys
_CACHE = {} _CACHE = {}
_IDS = defaultdict(int) _IDS = defaultdict(int)
@ -12,6 +13,14 @@ def get_propery(element, name):
return element.xpath('property[@name="%s"]' % name)[0].text 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): def melt_xml(file):
out = None out = None
real_path = os.path.realpath(file) real_path = os.path.realpath(file)
@ -20,7 +29,8 @@ def melt_xml(file):
if os.stat(real_path).st_mtime != ts: if os.stat(real_path).st_mtime != ts:
out = None out = None
if not out: if not out:
out = subprocess.check_output(['melt', file, '-consumer', 'xml']).decode() cmd = get_melt() + [file, '-consumer', 'xml']
out = subprocess.check_output(cmd).decode()
_CACHE[file] = [os.stat(real_path).st_mtime, out] _CACHE[file] = [os.stat(real_path).st_mtime, out]
return out return out
@ -554,7 +564,6 @@ class KDEnliveProject:
] + value) ] + value)
] ]
def properties(self, *props): def properties(self, *props):
return [ return [
self.get_element("property", attrib={"name": name}, text=str(value) if value is not None else value) self.get_element("property", attrib={"name": name}, text=str(value) if value is not None else value)

6
sax.py
View file

@ -31,7 +31,7 @@ reverb = {
"src": reverb_wav, "src": reverb_wav,
"duration": 3600.0, "duration": 3600.0,
"filter": { "filter": {
"volume": "0.5" "volume": "3.5"
}, },
} }
@ -39,14 +39,14 @@ long = {
"src": long_wav, "src": long_wav,
"duration": 3600.0, "duration": 3600.0,
"filter": { "filter": {
"volume": "-4" "volume": "-1"
}, },
} }
noise = { noise = {
"src": nois_wav, "src": nois_wav,
"duration": 3600.0, "duration": 3600.0,
"filter": { "filter": {
"volume": "4.75" "volume": "7.75"
}, },
} }

BIN
title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB