Compare commits

...

10 commits

Author SHA1 Message Date
j
d4372d0595 p_for_power 2025-11-27 18:09:06 +01:00
j
4e767b7268 fix default options 2025-11-27 18:02:00 +01:00
j
3f9280e0ba make font/spacing configuration option 2025-11-27 17:49:29 +01:00
j
2a5d741ccf use default options instead of passing all over the place 2025-11-21 11:00:40 +01:00
j
ce51e8c2c4 optionally render srt subtitles 2025-11-21 11:00:06 +01:00
j
790ae53095 switch to .ass subtitles to adjust per language font/size 2025-11-14 18:40:41 +01:00
j
b1db77de53 remove demo description 2025-11-14 15:23:58 +01:00
j
187d853b3a fix censored selection 2025-11-13 09:30:35 +01:00
j
992d39bc22 try to load config by default 2025-10-20 10:39:28 +01:00
j
bde25f5762 tag regexp strings 2025-10-20 10:33:34 +01:00
17 changed files with 335 additions and 339 deletions

View file

@ -21,7 +21,7 @@ https://textb.org/r/t_for_time_subtitles_5_ashley/
data = requests.get(url).text data = requests.get(url).text
parts = data.strip().split('##') parts = data.strip().split('##')
print(url) print(url)
prefix = '/srv/t_for_time/vo/' + url.split('/')[-2].split('subtitles_')[-1] prefix = '/srv/p_for_power/vo/' + url.split('/')[-2].split('subtitles_')[-1]
for part in parts: for part in parts:
part = part.strip().split('\n') part = part.strip().split('\n')
if part: if part:
@ -86,7 +86,7 @@ def update_subtitles():
wav = i.files.filter(selected=True)[0].data.path wav = i.files.filter(selected=True)[0].data.path
id = i.get('title').split('_')[0] id = i.get('title').split('_')[0]
batch = i.get('batch')[0][5:].lower().replace('-', '_').replace(' ', '') batch = i.get('batch')[0][5:].lower().replace('-', '_').replace(' ', '')
txt = '/srv/t_for_time/vo/%s_%s.txt' % (batch, id) txt = '/srv/p_for_power/vo/%s_%s.txt' % (batch, id)
if os.path.exists(txt): if os.path.exists(txt):
print(i, wav, txt) print(i, wav, txt)
subtitles = gentle2subtitles(align_text(txt, wav)) subtitles = gentle2subtitles(align_text(txt, wav))

View file

@ -1015,7 +1015,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
the system (from). the system (from).
*/ */
"site": { "site": {
"description": "This is a demo of pan.do/ra - a free, open source media archive. It allows you to manage large, decentralized collections of video, to collaboratively create metadata and time-based annotations, and to serve your archive as a cutting-edge web application.", "description": "P for Power - pan.do/ra",
"email": { "email": {
// E-mail address in contact form (to) // E-mail address in contact form (to)
"contact": "system@time.0x2620.org", "contact": "system@time.0x2620.org",
@ -1025,12 +1025,12 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
"system": "system@time.0x2620.org" "system": "system@time.0x2620.org"
}, },
"https": true, "https": true,
"id": "t_for_time", "id": "p_for_power",
"name": "T for Time", "name": "P for Power",
// Set to true to allow search engines to index the site // Set to true to allow search engines to index the site
"public": false, "public": false,
"sendReferrer": true, "sendReferrer": true,
"url": "time.0x2620.org" "url": "power.0x2620.org"
}, },
/* /*
"sitePages" defines the sections of the main site dialog. If "news" is "sitePages" defines the sections of the main site dialog. If "news" is
@ -1052,9 +1052,6 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
cross-instance references. cross-instance references.
*/ */
"sites": [ "sites": [
{"name": "0xDB", "url": "0xdb.org", "https": true},
{"name": "Pad.ma", "url": "pad.ma", "https": true},
{"name": "Indiancine.ma", "url": "indiancine.ma", "https": true}
], ],
/* /*
"textRightsLevels" defines a list of rights levels for texts. "textRightsLevels" defines a list of rights levels for texts.

View file

@ -100,7 +100,7 @@ if os.path.exists('__init__.py'):
local_settings += '\nLOCAL_APPS = ["%s"]\n' % name local_settings += '\nLOCAL_APPS = ["%s"]\n' % name
local_settings_changed = True local_settings_changed = True
else: else:
apps = re.compile('(LOCAL_APPS.*?)\]', re.DOTALL).findall(local_settings)[0] apps = re.compile(r'(LOCAL_APPS.*?)\]', re.DOTALL).findall(local_settings)[0]
if name not in apps: if name not in apps:
new_apps = apps.strip() + ',\n"%s"\n' % name new_apps = apps.strip() + ',\n"%s"\n' % name
local_settings = local_settings.replace(apps, new_apps) local_settings = local_settings.replace(apps, new_apps)

View file

@ -1,155 +1,13 @@
import json
import os
import re
import subprocess
from collections import defaultdict
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.conf import settings
import item.models from ...render import generate_clips, default_prefix
import itemlist.models
from ...render import get_srt
def resolve_roman(s):
extra = re.compile('^\d+(.*?)$').findall(s)
if extra:
extra = extra[0].lower()
new = {
'i': '1', 'ii': '2', 'iii': '3', 'iv': '4', 'v': '5',
'vi': '6', 'vii': 7, 'viii': '8', 'ix': '9', 'x': '10'
}.get(extra, extra)
return s.replace(extra, new)
return s
def format_duration(duration, fps):
return float('%0.5f' % (round(duration * fps) / fps))
class Command(BaseCommand): 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=default_prefix, help='prefix to build clips in')
parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in')
parser.add_argument('--censored', action='store', dest='censored', default=None, help='censor items from list')
def handle(self, **options): def handle(self, **options):
prefix = options['prefix'] return generate_clips(options)
lang = options["lang"]
if lang:
lang = lang.split(',')
tlang = lang[1:]
lang = lang[0]
else:
tlang = None
if lang == "en":
lang = None
if options['censored']:
censored_list = itemlist.models.List.get(options["censored"])
censored = list(censored_list.get_items(censored_list.user).all().values_list('public_id', flat=True))
clips = []
for i in item.models.Item.objects.filter(sort__type='original'):
original_target = ""
qs = item.models.Item.objects.filter(data__title=i.data['title']).exclude(id=i.id)
if qs.count() >= 1:
clip = {}
durations = []
for e in item.models.Item.objects.filter(data__title=i.data['title']):
if 'type' not in e.data:
print("ignoring invalid video %s (no type)" % e)
continue
if not e.files.filter(selected=True).exists():
continue
source = e.files.filter(selected=True)[0].data.path
ext = os.path.splitext(source)[1]
type_ = e.data['type'][0].lower()
target = os.path.join(prefix, type_, i.data['title'] + ext)
os.makedirs(os.path.dirname(target), exist_ok=True)
if os.path.islink(target):
os.unlink(target)
os.symlink(source, target)
if type_ == "original":
original_target = target
if options['censored'] and e.public_id in censored:
target = '/srv/t_for_time/censored.mp4'
clip[type_] = target
durations.append(e.files.filter(selected=True)[0].duration)
clip["duration"] = min(durations)
if not clip["duration"]:
print('!!', durations, clip)
continue
cd = format_duration(clip["duration"], 24)
#if cd != clip["duration"]:
# print(clip["duration"], '->', cd, durations, clip)
clip["duration"] = cd
clip['tags'] = i.data.get('tags', [])
clip['editingtags'] = i.data.get('editingtags', [])
name = os.path.basename(original_target)
seqid = re.sub("Hotel Aporia_(\d+)", "S\\1_", name)
seqid = re.sub("Night March_(\d+)", "S\\1_", seqid)
seqid = re.sub("_(\d+)H_(\d+)", "_S\\1\\2_", seqid)
seqid = seqid.split('_')[:2]
seqid = [b[1:] if b[0] in ('B', 'S') else '0' for b in seqid]
seqid[1] = resolve_roman(seqid[1])
seqid[1] = ''.join([b for b in seqid[1] if b.isdigit()])
if not seqid[1]:
seqid[1] = '0'
try:
clip['seqid'] = int(''.join(['%06d' % int(b) for b in seqid]))
except:
print(name, seqid, 'failed')
raise
if "original" in clip and "foreground" in clip and "background" in clip:
clips.append(clip)
elif "original" in clip and "animation" in clip:
clips.append(clip)
else:
print("ignoring incomplete video", i)
with open(os.path.join(prefix, 'clips.json'), 'w') as fd:
json.dump(clips, fd, indent=2, ensure_ascii=False)
print("using", len(clips), "clips")
voice_over = defaultdict(dict)
for vo in item.models.Item.objects.filter(
data__type__contains="Voice Over",
):
fragment_id = int(vo.get('title').split('_')[0])
source = vo.files.filter(selected=True)[0]
batch = vo.get('batch')[0].replace('Text-', '')
src = source.data.path
target = os.path.join(prefix, 'voice_over', batch, '%s.wav' % fragment_id)
os.makedirs(os.path.dirname(target), exist_ok=True)
if os.path.islink(target):
os.unlink(target)
os.symlink(src, target)
subs = []
for sub in vo.annotations.filter(layer="subtitles", languages=lang).exclude(value="").order_by("start"):
sdata = get_srt(sub, lang=tlang)
subs.append(sdata)
voice_over[fragment_id][batch] = {
"src": target,
"duration": format_duration(source.duration, 24),
"subs": subs
}
with open(os.path.join(prefix, 'voice_over.json'), 'w') as fd:
json.dump(voice_over, fd, indent=2, ensure_ascii=False)
if options['censored']:
censored_mp4 = '/srv/t_for_time/censored.mp4'
if not os.path.exists(censored_mp4):
cmd = [
"ffmpeg",
"-nostats", "-loglevel", "error",
"-f", "lavfi",
"-i", "color=color=white:size=1920x1080:rate=24",
"-t", "3600",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
censored_mp4
]
subprocess.call(cmd)

View file

@ -5,14 +5,14 @@ import subprocess
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 render_infinity from ...render import render_infinity, default_prefix
class Command(BaseCommand): class Command(BaseCommand):
help = 'render infinity' help = 'render infinity'
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=default_prefix, help='prefix to build clips in')
parser.add_argument('--config', action='store', dest='config', default=None, help='config') 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('--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('--single-file', action='store_true', dest='single_file', default=False, help='render to single video')

View file

@ -5,14 +5,14 @@ import subprocess
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 render_all from ...render import render_all, default_prefix
class Command(BaseCommand): class Command(BaseCommand):
help = 'genrate kdenlive porject and render' help = 'genrate kdenlive porject and render'
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=default_prefix, help='prefix to build clips in')
parser.add_argument('--duration', action='store', dest='duration', default="3600", help='target duration of all fragments in seconds') parser.add_argument('--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')

View file

@ -5,16 +5,15 @@ import subprocess
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 update_subtitles from ...render import update_subtitles, default_prefix
class Command(BaseCommand): class Command(BaseCommand):
help = 'genrate kdenlive porject and render' help = 'genrate kdenlive porject and render'
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=default_prefix, help='prefix to build clips in')
parser.add_argument('--offset', action='store', dest='offset', default="1024", help='inital offset in pi') parser.add_argument('--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

@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Exec=/srv/pandora/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u Exec=/srv/pandora/p_for_power/player/player.py --mode peer --playlist /srv/p_for_power/render/back.m3u
Hidden=false Hidden=false
NoDisplay=false NoDisplay=false
X-GNOME-Autostart-enabled=true X-GNOME-Autostart-enabled=true

View file

@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Exec=/srv/pandora/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u Exec=/srv/pandora/p_for_power/player/player.py --mode main --playlist /srv/p_for_power/render/front.m3u
Hidden=false Hidden=false
NoDisplay=false NoDisplay=false
X-GNOME-Autostart-enabled=true X-GNOME-Autostart-enabled=true

View file

@ -7,7 +7,7 @@ Wants=network-online.target
Type=simple Type=simple
Restart=on-failure Restart=on-failure
KillSignal=SIGINT KillSignal=SIGINT
ExecStart=/srv/pandora/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u ExecStart=/srv/pandora/p_for_power/player/player.py --mode peer --playlist /srv/p_for_power/render/back.m3u --config /srv/p_for_power/render/back.json
[Install] [Install]
WantedBy=graphical-session.target WantedBy=graphical-session.target

View file

@ -6,7 +6,7 @@ After=gnome-session.target network-online.target
Type=simple Type=simple
Restart=on-failure Restart=on-failure
KillSignal=SIGINT KillSignal=SIGINT
ExecStart=/srv/pandora/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u ExecStart=/srv/pandora/p_for_power/player/player.py --mode main --playlist /srv/p_for_power/render/front.m3u --config /srv/p_for_power/render/front.json
[Install] [Install]
WantedBy=graphical-session.target WantedBy=graphical-session.target

View file

@ -13,7 +13,7 @@ import mpv
import logging import logging
logger = logging.getLogger('t_for_time') logger = logging.getLogger('p_for_power')
SYNC_TOLERANCE = 0.05 SYNC_TOLERANCE = 0.05
SYNC_GRACE_TIME = 5 SYNC_GRACE_TIME = 5
@ -74,7 +74,7 @@ class Sync(Thread):
input_vo_keyboard=True, input_vo_keyboard=True,
) )
self.sax.loop_file = True self.sax.loop_file = True
self.sax.play("/srv/t_for_time/render/Saxophone-5.1.mp4") self.sax.play("/srv/p_for_power/render/Saxophone-5.1.mp4")
else: else:
self.sax = None self.sax = None
@ -408,11 +408,11 @@ class Sync(Thread):
def main(): def main():
prefix = os.path.expanduser('~/Videos/t_for_time') prefix = os.path.expanduser('~/Videos/p_for_power')
parser = argparse.ArgumentParser(description='t_for_time sync player') parser = argparse.ArgumentParser(description='p_for_power sync player')
parser.add_argument('--mode', help='peer or main', default="peer") parser.add_argument('--mode', help='peer or main', default="peer")
parser.add_argument('--playlist', default='/srv/t_for_time/render/128/front.m3u', help="m3u") parser.add_argument('--playlist', default='/srv/p_for_power/render/128/front.m3u', help="m3u")
parser.add_argument('--prefix', help='video location', default=prefix) parser.add_argument('--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)

View file

@ -1,8 +0,0 @@
[Desktop Entry]
Type=Application
Exec=/usr/bin/mpv --quiet --loop /srv/t_for_time/render/Saxophone-5.1.mp4
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
Name=loop
Comment=

View file

@ -1,11 +0,0 @@
[Unit]
Description=saxophone loop
[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/mpv --quiet --loop /srv/t_for_time/render/Saxophone-5.1.mp4
KillSignal=SIGINT
[Install]
WantedBy=graphical-session.target

350
render.py
View file

@ -16,6 +16,7 @@ import lxml.etree
from .pi import random from .pi import random
from .render_kdenlive import KDEnliveProject, _CACHE, melt_xml, get_melt from .render_kdenlive import KDEnliveProject, _CACHE, melt_xml, get_melt
default_prefix = "/srv/p_for_power"
def random_int(seq, length): def random_int(seq, length):
n = n_ = length - 1 n = n_ = length - 1
@ -61,7 +62,7 @@ def write_if_new(path, data, mode=''):
old = "" old = ""
is_new = data != old is_new = data != old
if path.endswith(".kdenlive"): if path.endswith(".kdenlive"):
is_new = re.sub('\{.{36}\}', '', data) != re.sub('\{.{36}\}', '', old) is_new = re.sub(r'\{.{36}\}', '', data) != re.sub(r'\{.{36}\}', '', old)
if is_new: if is_new:
with open(path, write_mode) as fd: with open(path, write_mode) as fd:
fd.write(data) fd.write(data)
@ -371,17 +372,33 @@ def get_offset_duration(prefix):
duration += get_scene_duration(scene) duration += get_scene_duration(scene)
return duration 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): def render(root, scene, prefix='', options=None):
if options is None: options = {} if options is None:
options = {}
fps = 24 fps = 24
files = [] files = []
scene_duration = int(get_scene_duration(scene) * fps) 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") folder = Path(root) / prefix
data = fix_overlaps(data) write_subtitles(data, folder, options)
srt = ox.srt.encode(data)
write_if_new(path, srt, 'b')
continue continue
#print(timeline) #print(timeline)
project = KDEnliveProject(root) project = KDEnliveProject(root)
@ -469,15 +486,32 @@ def get_fragments(clips, voice_over, prefix):
fragment['clips'] = [] fragment['clips'] = []
for clip in clips: for clip in clips:
#if set(clip['tags']) & set(fragment['tags']) and not set(clip['tags']) & set(fragment['anti-tags']): #if set(clip['tags']) & set(fragment['tags']) and not set(clip['tags']) & set(fragment['anti-tags']):
if clip['original'] in originals: key = 'original'
original = clip['original']
if 'original_censored' in clip:
original = clip['original_censored']
if original in originals:
fragment['clips'].append(clip) fragment['clips'].append(clip)
fragment["voice_over"] = voice_over.get(str(fragment["id"]), {}) fragment["voice_over"] = voice_over.get(str(fragment["id"]), {})
fragments.append(fragment) fragments.append(fragment)
fragments.sort(key=lambda f: ox.sort_string(f['name'])) fragments.sort(key=lambda f: ox.sort_string(f['name']))
return fragments 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): def render_all(options):
options = load_defaults(options)
prefix = options['prefix'] prefix = options['prefix']
duration = int(options['duration']) duration = int(options['duration'])
base = int(options['offset']) base = int(options['offset'])
@ -767,7 +801,7 @@ def render_all(options):
fn = base_prefix / fn fn = base_prefix / fn
if os.path.exists(fn): if os.path.exists(fn):
os.unlink(fn) os.unlink(fn)
join_subtitles(base_prefix) join_subtitles(base_prefix, options)
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))
@ -790,11 +824,39 @@ def add_translations(sub, lang):
value += '\n' + tvalue value += '\n' + tvalue
return value return value
def get_srt(sub, offset=0, lang=None): def add_translations_dict(sub, langs):
values = {}
value = sub.value.replace('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\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('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\n').strip()
if tsub.languages:
tvalue = ox.strip_tags(tvalue)
values[slang] = tvalue
return values
def get_srt(sub, offset, lang, tlang):
sdata = sub.json(keys=['in', 'out', 'value']) sdata = sub.json(keys=['in', 'out', 'value'])
sdata['value'] = sdata['value'].replace('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\n').strip() sdata['value'] = sdata['value'].replace('<br/>', '<br>').replace('<br>\n', '\n').replace('<br>', '\n').strip()
if lang: if tlang:
sdata['value'] = add_translations(sub, lang) sdata['value'] = add_translations(sub, tlang)
langs = [lang]
if tlang:
langs += tlang
sdata['values'] = add_translations_dict(sub, langs)
if offset: if offset:
sdata["in"] += offset sdata["in"] += offset
sdata["out"] += offset sdata["out"] += offset
@ -811,21 +873,55 @@ def fix_overlaps(data):
previous = sub previous = sub
return data return data
def shift_clips(data, offset):
for clip in data:
clip['in'] += offset
clip['out'] += offset
def scene_subtitles(scene, options):
import item.models
offset = 0
subs = []
lang, tlang = parse_lang(options["lang"])
for clip in scene['audio-center']['A1']:
if not clip.get("blank"):
batch, fragment_id = clip['src'].replace('.wav', '').split('/')[-2:]
vo = item.models.Item.objects.filter(
data__batch__icontains=batch, data__title__startswith=fragment_id + '_'
).first()
if vo:
#print("%s => %s %s" % (clip['src'], vo, vo.get('batch')))
for sub in vo.annotations.filter(
layer="subtitles"
).filter(
languages=None if lang == "en" else lang
).exclude(value="").order_by("start"):
sdata = get_srt(sub, offset, lang, tlang)
subs.append(sdata)
else:
print("could not find vo for %s" % clip['src'])
offset += clip['duration']
return subs
def load_defaults(options):
path = os.path.join(options["prefix"], "options.json")
if os.path.exists(path):
with open(path) as fd:
defaults = json.load(fd)
for key in defaults:
if key not in options:
options[key] = defaults[key]
return options
def update_subtitles(options): def update_subtitles(options):
import item.models import item.models
options = load_defaults(options)
prefix = Path(options['prefix']) prefix = Path(options['prefix'])
base = int(options['offset']) base = int(options['offset'])
lang = options["lang"] lang, tlang = parse_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):
@ -840,25 +936,59 @@ def update_subtitles(options):
continue continue
with open(scene_json) as fd: with open(scene_json) as fd:
scene = json.load(fd) scene = json.load(fd)
offset = 0 subs = scene_subtitles(scene, options)
subs = [] write_subtitles(subs, folder, options)
for clip in scene['audio-center']['A1']:
if not clip.get("blank"):
batch, fragment_id = clip['src'].replace('.wav', '').split('/')[-2:]
vo = item.models.Item.objects.filter(data__batch__icontains=batch, data__title__startswith=fragment_id + '_').first()
if vo:
#print("%s => %s %s" % (clip['src'], vo, vo.get('batch')))
for sub in vo.annotations.filter(layer="subtitles").filter(languages=lang).exclude(value="").order_by("start"):
sdata = get_srt(sub, offset, tlang)
subs.append(sdata)
else:
print("could not find vo for %s" % clip['src'])
offset += clip['duration']
path = folder / "front.srt"
data = fix_overlaps(subs)
srt = ox.srt.encode(subs)
write_if_new(str(path), srt, 'b')
def ass_encode(subs, options):
if "lang" in options:
langs = options["lang"].split(',')
else:
langs = list(subs[0]["values"])
#print('ass_encode', langs, options)
#print(subs)
header = '''[Script Info]
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080
ScaledBorderAndShadow: yes
YCbCr Matrix: None
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
'''
ass = header
offset = options.get("sub_margin", 10)
spacing = options.get("sub_spacing", 20)
height = 42
styles = []
for lang in reversed(langs):
if isinstance(options.get("font"), list) and lang in options["font"]:
font = options["font"][lang]
else:
font = 'SimHei' if lang in ('zh', 'jp') else 'Menlo'
if isinstance(options.get("font_size"), list) and lang in options["font_size"]:
size = options["font_size"][lang]
else:
size = 46 if font == 'SimHei' else 42
styles.append(
f'Style: {lang},{font},{size},&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,{offset},1'
)
offset += size + spacing
ass += '\n'.join(reversed(styles)) + '\n'
events = [
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
]
for sub in subs:
start = ox.format_timecode(sub['in']).rstrip('0')
stop = ox.format_timecode(sub['out']).rstrip('0')
for lang in reversed(langs):
value = sub['values'][lang]
event = f'Dialogue: 0,{start},{stop},{lang},,0,0,0,,{value}'
events.append(event)
ass += '\n\n[Events]\n' + '\n'.join(events) + '\n'
return ass
def update_m3u(render_prefix, exclude=[]): def update_m3u(render_prefix, exclude=[]):
files = ox.sorted_strings(glob(render_prefix + "*/*/back.mp4")) files = ox.sorted_strings(glob(render_prefix + "*/*/back.mp4"))
@ -931,7 +1061,8 @@ def render_infinity(options):
shutil.move(state_f + "~", state_f) shutil.move(state_f + "~", state_f)
def join_subtitles(base_prefix): def join_subtitles(base_prefix, options):
'''
subtitles = list(sorted(glob('%s/*/front.srt' % base_prefix))) subtitles = list(sorted(glob('%s/*/front.srt' % base_prefix)))
data = [] data = []
position = 0 position = 0
@ -941,3 +1072,142 @@ def join_subtitles(base_prefix):
position += get_scene_duration(scene) position += get_scene_duration(scene)
with open(base_prefix / 'front.srt', 'wb') as fd: with open(base_prefix / 'front.srt', 'wb') as fd:
fd.write(ox.srt.encode(data)) fd.write(ox.srt.encode(data))
'''
scenes = list(sorted(glob('%s/*/scene.json' % base_prefix)))
data = []
position = 0
for scene in scenes:
subs = scene_subtitles(scene, options)
data += shift_clips(subs, position)
position += get_scene_duration(scene)
write_subtitles(data, base_prefix, options)
def resolve_roman(s):
extra = re.compile(r'^\d+(.*?)$').findall(s)
if extra:
extra = extra[0].lower()
new = {
'i': '1', 'ii': '2', 'iii': '3', 'iv': '4', 'v': '5',
'vi': '6', 'vii': 7, 'viii': '8', 'ix': '9', 'x': '10'
}.get(extra, extra)
return s.replace(extra, new)
return s
def generate_clips(options):
import item.models
import itemlist.models
options = load_defaults(options)
prefix = options['prefix']
lang, tlang = parse_lang(options["lang"])
if options['censored']:
censored_list = itemlist.models.List.get(options["censored"])
censored = list(censored_list.get_items(
censored_list.user
).all().values_list('public_id', flat=True))
clips = []
for i in item.models.Item.objects.filter(sort__type='original'):
original_target = ""
qs = item.models.Item.objects.filter(data__title=i.data['title']).exclude(id=i.id)
if qs.count() >= 1:
clip = {}
durations = []
for e in item.models.Item.objects.filter(data__title=i.data['title']):
if 'type' not in e.data:
print("ignoring invalid video %s (no type)" % e)
continue
if not e.files.filter(selected=True).exists():
continue
source = e.files.filter(selected=True)[0].data.path
ext = os.path.splitext(source)[1]
type_ = e.data['type'][0].lower()
target = os.path.join(prefix, type_, i.data['title'] + ext)
os.makedirs(os.path.dirname(target), exist_ok=True)
if os.path.islink(target):
os.unlink(target)
os.symlink(source, target)
if type_ == "original":
original_target = target
if options['censored'] and e.public_id in censored:
clip[type_ + "_censored"] = target
target = '/srv/t_for_time/censored.mp4'
clip[type_] = target
durations.append(e.files.filter(selected=True)[0].duration)
clip["duration"] = min(durations)
if not clip["duration"]:
print('!!', durations, clip)
continue
cd = format_duration(clip["duration"], 24)
#if cd != clip["duration"]:
# print(clip["duration"], '->', cd, durations, clip)
clip["duration"] = cd
clip['tags'] = i.data.get('tags', [])
clip['editingtags'] = i.data.get('editingtags', [])
name = os.path.basename(original_target)
seqid = re.sub(r"Hotel Aporia_(\d+)", "S\\1_", name)
seqid = re.sub(r"Night March_(\d+)", "S\\1_", seqid)
seqid = re.sub(r"_(\d+)H_(\d+)", "_S\\1\\2_", seqid)
seqid = seqid.split('_')[:2]
seqid = [b[1:] if b[0] in ('B', 'S') else '0' for b in seqid]
seqid[1] = resolve_roman(seqid[1])
seqid[1] = ''.join([b for b in seqid[1] if b.isdigit()])
if not seqid[1]:
seqid[1] = '0'
try:
clip['seqid'] = int(''.join(['%06d' % int(b) for b in seqid]))
except:
print(name, seqid, 'failed')
raise
if "original" in clip and "foreground" in clip and "background" in clip:
clips.append(clip)
elif "original" in clip and "animation" in clip:
clips.append(clip)
else:
print("ignoring incomplete video", i)
with open(os.path.join(prefix, 'clips.json'), 'w') as fd:
json.dump(clips, fd, indent=2, ensure_ascii=False)
print("using", len(clips), "clips")
voice_over = defaultdict(dict)
for vo in item.models.Item.objects.filter(
data__type__contains="Voice Over",
):
fragment_id = int(vo.get('title').split('_')[0])
source = vo.files.filter(selected=True)[0]
batch = vo.get('batch')[0].replace('Text-', '')
src = source.data.path
target = os.path.join(prefix, 'voice_over', batch, '%s.wav' % fragment_id)
os.makedirs(os.path.dirname(target), exist_ok=True)
if os.path.islink(target):
os.unlink(target)
os.symlink(src, target)
subs = []
for sub in vo.annotations.filter(
layer="subtitles", languages=lang
).exclude(value="").order_by("start"):
sdata = get_srt(sub, 0, lang, tlang)
subs.append(sdata)
voice_over[fragment_id][batch] = {
"src": target,
"duration": format_duration(source.duration, 24),
"subs": subs
}
with open(os.path.join(prefix, 'voice_over.json'), 'w') as fd:
json.dump(voice_over, fd, indent=2, ensure_ascii=False)
if options['censored']:
censored_mp4 = '/srv/t_for_time/censored.mp4'
if not os.path.exists(censored_mp4):
cmd = [
"ffmpeg",
"-nostats", "-loglevel", "error",
"-f", "lavfi",
"-i", "color=color=white:size=1920x1080:rate=24",
"-t", "3600",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
censored_mp4
]
subprocess.call(cmd)

109
sax.py
View file

@ -1,109 +0,0 @@
#!/usr/bin/python3
import os
from render_kdenlive import KDEnliveProject, _CACHE
import subprocess
def generate_sax_mix(root):
os.chdir(root)
root = os.path.abspath(".")
long_wav = "Soon_Kim_Long_Reverb_Only2.wav"
nois_wav = "Soon_Kim_Noise.wav"
reverb_wav = "Soon_Kim_Short_Reverb_Mix2.wav"
'''
i = item.models.Item.objects.get(data__title='Soon_Kim_Long_Reverb_Only2')
i.files.all()[0].data.path
'/srv/pandora/data/media/6b/44/16/3f2905e886/data.wav'
i = item.models.Item.objects.get(data__title='Soon_Kim_Short_Reverb_Mix2')
i.files.all()[0].data.path
'/srv/pandora/data/media/ee/e0/04/d4ab42c3de/data.wav'
i = item.models.Item.objects.get(data__title='Soon_Kim_Noise')
i.files.all()[0].data.path
'/srv/pandora/data/media/84/88/87/d2fb2e2dc2/data.wav'
'''
reverb = {
"src": reverb_wav,
"duration": 3600.0,
"filter": {
"volume": "3.5"
},
}
long = {
"src": long_wav,
"duration": 3600.0,
"filter": {
"volume": "-1"
},
}
noise = {
"src": nois_wav,
"duration": 3600.0,
"filter": {
"volume": "7.75"
},
}
project = KDEnliveProject(root)
project.append_clip('A1', long)
project.append_clip('A2', noise)
path = os.path.join(root, "sax-mix.kdenlive")
with open(path, 'w') as fd:
fd.write(project.to_xml())
project = KDEnliveProject(root)
project.append_clip('A1', reverb)
path = os.path.join(root, "sax-reverb-mix.kdenlive")
with open(path, 'w') as fd:
fd.write(project.to_xml())
cmds = []
cmds.append([
"melt", "sax-mix.kdenlive", '-quiet', '-consumer', 'avformat:sax-mix.wav'
])
cmds.append([
"melt", "sax-reverb-mix.kdenlive", '-quiet', '-consumer', 'avformat:sax-reverb-mix.wav'
])
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-f", "lavfi", "-i", "anullsrc=r=48000:cl=mono", "-t", "3600", "silence.wav"
])
for src, out1, out2 in (
('sax-reverb-mix.wav', "fl.wav", "fr.wav"),
("sax-mix.wav", "bl.wav", "br.wav"),
):
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-i", src,
"-filter_complex",
"[0:0]pan=1|c0=c0[left]; [0:0]pan=1|c0=c1[right]",
"-map", "[left]", out1,
"-map", "[right]", out2,
])
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-i", "fl.wav",
"-i", "fr.wav",
"-i", "silence.wav",
"-i", "silence.wav",
"-i", "bl.wav",
"-i", "br.wav",
"-filter_complex", "[0:a][1:a][2:a][3:a][4:a][5:a]amerge=inputs=6[a]",
"-map", "[a]",
"-ar", "48000",
"-c:a", "aac", "Saxophone-5.1.mp4"
])
for cmd in cmds:
print(" ".join([str(x) for x in cmd]))
subprocess.call(cmd)