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 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):
pass
parser.add_argument('--lang', action='store', dest='lang', default=None, help='subtitle language')
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='').order_by("start"):
if not sub.languages:
print(sub.value.strip() + "\n")
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('<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")

View file

@ -23,15 +23,28 @@ def resolve_roman(s):
return s.replace(extra, new)
return s
def format_duration(duration, fps):
return float('%0.5f' % (round(duration * fps) / fps))
class Command(BaseCommand):
help = 'generate symlinks to clips and clips.json'
def add_arguments(self, parser):
parser.add_argument('--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')
def handle(self, **options):
prefix = options['prefix']
lang = options["lang"]
if lang:
lang = lang.split(',')
tlang = lang[1:]
lang = lang[0]
else:
tlang = None
if lang == "en":
lang = None
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)
@ -58,6 +71,10 @@ class Command(BaseCommand):
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(clip['original'])
@ -102,12 +119,12 @@ class Command(BaseCommand):
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)
for sub in vo.annotations.filter(layer="subtitles", languages=lang).exclude(value="").order_by("start"):
sdata = get_srt(sub, lang=tlang)
subs.append(sdata)
voice_over[fragment_id][batch] = {
"src": target,
"duration": source.duration,
"duration": format_duration(source.duration, 24),
"subs": subs
}
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):
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('--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):
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('--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('--debug', action='store_true', dest='debug', default=False, help='output more info')
def handle(self, **options):
render_all(options)

View file

@ -13,8 +13,8 @@ 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')
parser.add_argument('--lang', action='store', dest='lang', default=None, help='subtitle language')
def handle(self, **options):
update_subtitles(options)

View file

@ -8,6 +8,7 @@ import time
from threading import Thread
from datetime import datetime
import ox
import mpv
@ -19,10 +20,17 @@ SYNC_GRACE_TIME = 5
SYNC_JUMP_AHEAD = 1
PORT = 9067
DEBUG = False
FONT = 'Menlo'
FONT_SIZE = 30
FONT_BORDER = 4
SUB_MARGIN = 2 * 36 + 6
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,
}
def hide_gnome_overview():
@ -44,6 +52,7 @@ class Main:
class Sync(Thread):
active = True
is_main = True
is_paused = False
ready = False
destination = "255.255.255.255"
reload_check = None
@ -53,32 +62,52 @@ 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=FONT_SIZE, sub_font=FONT,
sub_border_size=FONT_BORDER,
sub_margin_y=SUB_MARGIN,
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"],
)
else:
self.mpv = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True,
sub_text_font_size=FONT_SIZE, sub_text_font=FONT,
sub_border_size=FONT_BORDER,
sub_margin_y=SUB_MARGIN,
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"],
)
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)
@ -90,6 +119,31 @@ 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()
@ -106,16 +160,45 @@ class Sync(Thread):
else:
self.read_position_main()
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")
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:
@ -195,6 +278,8 @@ 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:
@ -202,18 +287,47 @@ 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)
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")
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
else:
self.main.time_pos = float(data[0])
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('--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:
with open(args.config) as fd:
CONFIG.update(json.load(fd))
base = os.path.dirname(os.path.abspath(__file__))
#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:
try:
player.mpv.wait_for_playback()

284
render.py
View file

@ -11,8 +11,10 @@ import time
from pathlib import Path
import ox
import lxml.etree
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):
@ -64,8 +66,11 @@ def write_if_new(path, data, mode=''):
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):
fps = 24
length = 0
scene = {
'front': {
@ -100,6 +105,7 @@ def compose(clips, target=150, base=1024, voice_over=None):
used = []
voice_overs = []
sub_offset = 0
if voice_over:
vo_keys = list(sorted(voice_over))
if chance(seq, 0.5):
@ -118,7 +124,7 @@ def compose(clips, target=150, base=1024, voice_over=None):
if vo_min > target:
target = vo_min
elif vo_min < target:
offset = (target - vo_min) / 2
offset = format_duration((target - vo_min) / 2, fps)
scene['audio-center']['A1'].append({
'blank': True,
'duration': offset
@ -132,17 +138,17 @@ def compose(clips, target=150, base=1024, voice_over=None):
subs = []
for vo in voice_overs:
voc = vo.copy()
a, b = '3', '-6'
a, b = '-11', '-3'
if 'Whispered' in voc['src']:
a, b = '6', '-3'
a, b = '-8', '0'
elif 'Read' in voc['src']:
a, b = '6.25', '-2.75'
a, b = '-7.75', '0.25'
elif 'Free' in voc['src']:
a, b = '5.2', '-3.8'
a, b = '-8.8', '-0.8'
elif 'Ashley' in voc['src']:
a, b = '3.75', '-5.25'
a, b = '-9.5', '-1.50'
elif 'Melody' in voc['src']:
a, b = '4.25', '-4.75'
a, b = '-5.25', '-0.25'
voc['filter'] = {'volume': a}
scene['audio-center']['A1'].append(voc)
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:
break
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:
fg = clip['animation']
@ -282,10 +288,10 @@ def compose(clips, target=150, base=1024, voice_over=None):
scene['audio-back']['A1'].append({
'duration': clip['duration'],
'src': clip['original'],
'filter': {'volume': '+0.2'},
'filter': {'volume': '-8.2'},
})
# TBD: Foley
cf_volume = '-5.5'
cf_volume = '-2.5'
scene['audio-front']['A2'].append({
'duration': clip['duration'],
'src': foley,
@ -298,20 +304,61 @@ def compose(clips, target=150, base=1024, voice_over=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 += clip['duration']
return duration
duration += int(clip['duration'] * 24)
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
files = []
scene_duration = int(get_scene_duration(scene) * 24)
scene_duration = int(get_scene_duration(scene) * fps)
for timeline, data in scene.items():
if timeline == "subtitles":
path = os.path.join(root, prefix + "front.srt")
@ -328,21 +375,43 @@ def render(root, scene, prefix=''):
#print(track)
for clip in clips:
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-'):
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/24})
break
project.append_clip(track, {'blank': True, "duration": delta/fps})
path = os.path.join(root, prefix + "%s.kdenlive" % timeline)
project_xml = project.to_xml()
write_if_new(path, project_xml)
if options["debug"]:
# check duration
out_duration = get_project_duration(path)
p_duration = project.get_duration()
print(path, 'out: %s, project: %s, scene: %s' %(out_duration, p_duration, scene_duration))
if p_duration != scene_duration:
print(path, 'FAIL project: %s, scene: %s' %(p_duration, scene_duration))
_cache = os.path.join(root, "cache.json")
with open(_cache, "w") as fd:
json.dump(_CACHE, fd)
sys.exit(1)
if out_duration != p_duration:
print(path, 'fail got: %s expected: %s' %(out_duration, p_duration))
sys.exit(1)
files.append(path)
return files
def get_project_duration(file):
out = melt_xml(file)
chain = lxml.etree.fromstring(out).xpath('producer')[0]
duration = int(chain.attrib['out']) + 1
return duration
def get_fragments(clips, voice_over, prefix):
import itemlist.models
import item.models
@ -444,20 +513,19 @@ def render_all(options):
elif position < target_position:
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)
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:
print(timeline)
ext = '.mp4'
if '/audio' in timeline:
ext = '.wav'
cmd = [
'xvfb-run', '-a',
'melt', timeline,
cmd = get_melt() + [
timeline,
'-quiet',
'-consumer', 'avformat:%s' % timeline.replace('.kdenlive', ext),
]
@ -479,8 +547,8 @@ def render_all(options):
subprocess.call(cmd)
os.unlink(timeline.replace('.kdenlive', ext))
fragment_prefix = Path(fragment_prefix)
cmds = []
fragment_prefix = Path(fragment_prefix)
for src, out1, out2 in (
("audio-front.wav", "fl.wav", "fr.wav"),
("audio-center.wav", "fc.wav", "lfe.wav"),
@ -524,7 +592,8 @@ def render_all(options):
fragment_prefix / "back-audio.mp4",
])
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)
for a, b in (
@ -539,9 +608,13 @@ def render_all(options):
sys.exit(-1)
shutil.move(fragment_prefix / "back-audio.mp4", fragment_prefix / "back.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 (
"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",
"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):
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(json.dumps(dict(stats), sort_keys=True, indent=2))
with open(_cache, "w") as 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['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:
sdata["in"] += offset
sdata["out"] += offset
@ -578,8 +768,17 @@ def update_subtitles(options):
import item.models
prefix = Path(options['prefix'])
duration = int(options['duration'])
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")
if os.path.exists(_cache):
@ -589,7 +788,10 @@ def update_subtitles(options):
base_prefix = prefix / 'render' / str(base)
for folder in os.listdir(base_prefix):
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)
offset = 0
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()
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)
for sub in vo.annotations.filter(layer="subtitles").filter(languages=lang).exclude(value="").order_by("start"):
sdata = get_srt(sub, offset, tlang)
subs.append(sdata)
else:
print("could not find vo for %s" % clip['src'])
@ -646,7 +848,7 @@ def render_infinity(options):
"max-items": 30,
"no_video": False,
}
for key in ("prefix", "duration"):
for key in ("prefix", "duration", "debug", "single_file", "keep_audio"):
state[key] = options[key]
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 len(current) > state["max-items"]:
current = list(reversed(ox.sorted_strings(current)))
remove = list(reversed(current[-state["max-items"]:]))
current = ox.sorted_strings(current)
remove = current[:-state["max-items"]]
update_m3u(render_prefix, exclude=remove)
for folder in remove:
folder = render_prefix + folder
@ -675,3 +877,15 @@ 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):
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 uuid
import os
import sys
_CACHE = {}
_IDS = defaultdict(int)
@ -12,6 +13,14 @@ 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)
@ -20,7 +29,8 @@ def melt_xml(file):
if os.stat(real_path).st_mtime != ts:
out = None
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]
return out
@ -554,7 +564,6 @@ 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)

6
sax.py
View file

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

BIN
title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB