Compare commits

..

142 commits
master ... main

Author SHA1 Message Date
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
j
a229194d48 actually cleanup, only remove renders in infinity range 2024-03-19 10:12:15 +01:00
j
3d5293e7de lower priority 2024-03-19 10:11:40 +01:00
j
6e4e8ca7a6 to infinity 2024-03-10 18:14:59 +00:00
j
582bcf1813 export subtitles 2024-02-01 14:53:45 +01:00
j
9493eefaa3 adjust volume levels
- CF (-1.5  db)
- Melody (Ban's vocals) (+ 1 db)
- Soon Kim_Short Reverb ( Saxophone track ) (- 0.5 db)
- Soon Kim “noise” (+0.5 db)
2024-01-12 11:52:14 +01:00
j
5f0bc63684 fix subtitle overlaps 2023-12-08 12:13:00 +00:00
j
0505631c0e Melody +0.25db 2023-12-08 10:23:03 +00:00
j
f24bb26ed1 fix realpath 2023-11-24 15:29:42 +00:00
j
e2cd0861a3 fix typo 2023-11-24 13:19:28 +00:00
j
ddb70beedd invalidate cache if file changed 2023-11-24 13:13:56 +00:00
j
5295d1bfc9 bring down Ashley vocals slightly (-0.75 db) 2023-11-24 13:06:35 +00:00
j
34ef701646 hide gnome overview on startup 2023-11-20 23:27:49 +00:00
j
7008fa7e1f don't use same base for each fragment 2023-11-20 23:08:48 +00:00
j
37a2674f23 fine tune sync by notching playback speed 2023-11-20 12:32:07 +00:00
j
8194783064 reset sync on file change 2023-11-20 11:39:02 +00:00
j
73774b5293 reset deviation on file change 2023-11-20 11:28:47 +00:00
j
8db3a0f6e9 typo 2023-11-20 09:04:10 +00:00
j
ff7dcf6c54 more tweaking 2023-11-20 09:03:41 +00:00
j
2316c63156 add numberoftags 2023-11-19 09:29:01 +01:00
j
82ed65096e column widths 2023-11-19 09:26:26 +01:00
j
c384d03c96 listColumns 2023-11-19 09:24:39 +01:00
j
034e2771ee better list view 2023-11-19 09:21:18 +01:00
j
b6293db4b8 tune mix 2023-11-19 09:21:12 +01:00
j
94fa6d0e28 log list name 2023-11-18 21:31:45 +01:00
j
1f14fc216d tweak clip selection 2023-11-18 21:28:22 +01:00
j
559dd46285 fix info page 2023-11-18 21:11:39 +01:00
j
e035675214 more info 2023-11-18 21:07:11 +01:00
j
de1c704a3f sort by title 2023-11-17 17:36:25 +01:00
j
89918e141c colors and sort 2023-11-17 17:33:33 +01:00
j
1770544a59 batch colors 2023-11-17 17:18:05 +01:00
j
0191fc1769 poster batch 2023-11-17 17:14:59 +01:00
j
ea83be1b7f more debug info 2023-11-17 01:13:28 +01:00
j
e4793e816c don't sync in first second 2023-11-17 01:05:50 +01:00
j
ecbd30c460 fix pi 2023-11-16 16:17:25 +01:00
j
2dfdbab199 s/,/./g 2023-11-16 15:12:03 +01:00
j
b4c4a2fabb get_srt 2023-11-16 09:12:53 +01:00
j
5103e8f81b update subtitles 2023-11-16 09:08:03 +01:00
j
798efa43a8 tweak font size 2023-11-15 11:04:36 +01:00
j
c8cb320d4d stop on error 2023-11-15 11:01:51 +01:00
j
2d8dfa2ec5 move up 2023-11-15 11:00:22 +01:00
j
a83781ef56 move subs up 2023-11-15 09:39:44 +01:00
j
6823002315 subtitle aligment 2023-11-14 20:04:33 +01:00
j
4de39b926a subtitle tweaks 2023-11-14 19:52:12 +01:00
j
f00a023641 render subtitles 2023-11-14 17:49:01 +01:00
j
efd5e7310a sync reads position, log sync 2023-11-14 16:32:27 +01:00
j
a604bdfab9 sync on main thread 2023-11-14 12:26:33 +01:00
j
8e9236a176 volume tweaks 2023-11-14 11:12:18 +01:00
j
589ccf9fb7 no volume adjustment for noise 2023-11-13 19:05:15 +01:00
j
d66d66fb49 cleanup more wav 2023-11-12 23:38:46 +01:00
j
3c7f5a0d7d fix chance 2023-11-12 22:47:57 +01:00
j
88a8b84d04 better random 2023-11-12 18:30:06 +01:00
j
3b9d8f7275 remove more temp files 2023-11-09 01:13:05 +01:00
j
67eaf22ae9 melt bug with a/v timeline not resulting in correct length. render a and v and join after that 2023-11-09 01:11:51 +01:00
j
069eec802d fix audio duration 2023-11-09 00:33:06 +01:00
j
206183f5da fix player 2023-11-09 00:32:32 +01:00
j
c2e6fcc173 abort if front/back are not the same duration 2023-11-08 23:55:08 +01:00
j
60bd70704f quick if stuck 2023-11-08 23:46:07 +01:00
j
4e04bec8ad add systemd service. 2023-11-08 23:31:06 +01:00
j
a33f94d5af retry 2023-11-08 23:04:17 +01:00
j
841a654254 more advanced fragment parsing 2023-11-08 11:01:20 +01:00
j
3252392eb5 reset player if stuck 2023-11-08 11:01:02 +01:00
j
7e4c062a13 set timeout 2023-11-08 10:23:06 +01:00
j
67b67d87fa sax path notes 2023-11-08 09:19:07 +01:00
j
7bc8cd1bee log file played 2023-11-08 09:18:55 +01:00
j
1efa46b814 ignore no type 2023-11-08 09:08:38 +01:00
j
ca5ee40ca9 center filter 2023-11-08 00:10:28 +01:00
j
60f66b1cb4 volume adjustments 2023-11-08 00:04:37 +01:00
j
1608b5cb60 anti-tags, scale transparancy from 0-1, disable transparancy 2023-11-05 10:18:54 +01:00
j
eea27c5f8c don't crash just skip adjustment if player is still loading 2023-11-05 09:26:02 +01:00
j
10b6c1a2d0 5.1 saxophone mix 2023-11-03 23:06:01 +01:00
j
7368316f8d new mix 2023-11-03 22:15:20 +01:00
j
c786f22ed3 avoid repetition 2023-11-01 09:23:00 +01:00
j
dcd2030799 fix loop 2023-10-31 11:18:28 +01:00
j
05905ea2b5 reload playlist if changed 2023-10-31 10:03:57 +01:00
j
eb49a320d1 disable original transparancy for now 2023-10-31 09:01:04 +01:00
j
0d554bda05 player 2023-10-30 20:40:25 +01:00
j
b09b7f14d4 formating 2023-10-30 20:25:46 +01:00
j
21d3225269 fail for 0 lenght clips 2023-10-29 19:57:39 +01:00
j
120e7895b0 formating 2023-10-29 19:51:26 +01:00
j
441ebfa05c cut down clips towards end to fit within limit 2023-10-29 16:03:40 +01:00
j
3c5273ba5d make sure full voice over fits 2023-10-28 20:26:33 +02:00
j
35767b9105 fix symlink check 2023-10-28 15:12:45 +02:00
j
ff752a1818 fix V1/V2 naming, handle animation players, don't use blank, results in white 2023-10-28 15:03:35 +02:00
j
4058ac84c1 50% change try to continue with next clip in sequence 2023-10-28 11:58:07 +02:00
j
198934e465 get all originals, also get those with only animation player 2023-10-28 11:57:44 +02:00
j
056bc1e6ba use animation foreground layer 2023-10-28 11:33:29 +02:00
j
3a367e8c73 handle animation clips 2023-10-28 11:25:45 +02:00
j
fee8e821f9 mixed output 2023-10-28 11:24:51 +02:00
j
b571bb2271 import handling 2023-10-28 11:24:43 +02:00
j
af8f7433c3 produce better seeking mp4 files 2023-10-22 11:18:03 +01:00
j
c1c7fc27d3 update items 2023-10-21 15:46:08 +01:00
j
215f0db071 islink not exists 2023-10-21 15:46:03 +01:00
j
ecd88fe8a2 ignore empty items 2023-10-21 15:36:08 +01:00
j
de96448137 ignore 2023-10-21 15:33:35 +01:00
j
fe62266c4f utils to merge and cleanup 2023-10-21 15:33:28 +01:00
j
17d6da8282 only use selected files 2023-10-21 15:32:55 +01:00
j
72ab1b1d13 we might have foreground3 too 2023-10-20 16:20:16 +01:00
j
73a9481ef1 fix off by one and make option to load melt outside of class 2023-10-19 15:53:18 +01:00
j
7fe4b02552 use batch for vo 2023-10-19 15:52:55 +01:00
j
6071b71752 load editing tags 2023-10-19 15:52:35 +01:00
j
4491abc277 add editing tags 2023-10-19 15:52:12 +01:00
j
e0b157de64 only create 5.1 mix once 2023-10-19 13:10:23 +01:00
j
b1ca39ff2b fix links 2023-10-18 16:27:06 +01:00
j
59da950a20 poster 2023-10-18 16:23:29 +01:00
j
9c778bb7de add voice over, output 6 channels, add 5.1 mix 2023-10-16 23:26:09 +01:00
j
033fe8b2b5 fix batch edit 2023-10-12 17:01:20 +01:00
j
1d0e4ab296 fix poster flag 2023-10-11 14:50:42 +01:00
j
d113c5d79e render all fragments 2023-10-10 16:16:59 +01:00
j
a5957fc3b2 50% chance of second foreground 2023-10-10 10:19:47 +01:00
j
d625ad8f4c render audio only 2023-10-10 10:19:36 +01:00
j
e10dfe039f fix sound 2023-10-10 10:19:24 +01:00
j
5075394318 render fixes, encode vides in render command 2023-10-09 20:29:11 +01:00
j
e84ea31147 tweaks 2023-10-09 14:10:34 +01:00
j
debe1837a7 add render command 2023-10-08 12:19:05 +01:00
j
950287a2f7 fix generate_clips 2023-10-08 12:07:57 +01:00
j
21283e38ac generate clips 2023-10-08 12:06:24 +01:00
j
c48f227dfc link cluster 2023-10-06 14:26:13 +02:00
j
0a549ffddc type updates poster 2023-10-06 13:51:41 +02:00
j
5f1fd82472 time is python module id 2023-10-06 13:37:38 +02:00
j
6d59eb4a5a t-for-time 2023-10-06 13:31:43 +02:00
29 changed files with 4715 additions and 26 deletions

23
.editorconfig Normal file
View file

@ -0,0 +1,23 @@
[*]
end_of_line = lf
insert_final_newline = true
[*.py]
indent_style = space
indent_size = 4
[*.sass]
indent_style = space
indent_size = 2
[*.scss]
indent_style = space
indent_size = 2
[*.html]
indent_style = space
indent_size = 4
[*.js]
indent_style = space
indent_size = 4

2
.gitignore vendored
View file

@ -1 +1,3 @@
*.pyc
*.swp
*.swo

View file

@ -1,26 +1,10 @@
# pan.do/ra site config overlay
# pan.do/ra overlay for t-for-time
fork this repo into pandora_sitename and add your pan.do/ra customizations
pandora settings and render pipeline.
place your config as config.jsonc, add custom files to static/js, poster scripts to scripts
`apt install melt kdenlive ladspa-sdk xvfb`
custom files should be in the form <file>.<sitename>.js
`pandoractl manage genreate_clips`
i.e. js overlay:
`pandoractl manage render`
static/js/home.<sitename>.js
png overly the same i.e.
static/png/icon.<sitename>.png
poster/icon script without <sitename>:
script/item_icon.py
script/list_icon.py
script/potser.py
if you need a custom django module, touch __init__.py and install.py will take care of that too.
to use js pages from other sites, add them to overwrite in install.py
to deploy, checkout your fork into /srv/pandora/pandora/<sitename> and run ./install.py

98
align_subtitles.py Normal file
View file

@ -0,0 +1,98 @@
import requests
import json
import os
import subprocess
'''
apt-get install -y podman
podman run -P -p 8765:8765 lowerquality/gentle
'''
def load_subs():
subtitles = {}
for url in """
https://textb.org/r/t_for_time_subtitles_1_melodic/
https://textb.org/r/t_for_time_subtitles_2_whispered/
https://textb.org/r/t_for_time_subtitles_3_free/
https://textb.org/r/t_for_time_subtitles_4_read/
https://textb.org/r/t_for_time_subtitles_5_ashley/
""".strip().split('\n'):
data = requests.get(url).text
parts = data.strip().split('##')
print(url)
prefix = '/srv/t_for_time/vo/' + url.split('/')[-2].split('subtitles_')[-1]
for part in parts:
part = part.strip().split('\n')
if part:
title = part[0]
text = "\n".join(part[1:]).strip()
if text:
fname = '%s_%s.txt' % (prefix, title)
with open(fname, 'w') as fd:
fd.write(text)
def gentle2subtitles(align):
new_block = '\r\n\r\n'
if new_block not in align['transcript']:
new_block = '\n\n'
data = []
end = 0
for block in align['transcript'].split(new_block):
if not block.strip():
continue
start = end
end += len(block)
in_ = -1
out_ = -1
for word in align['words']:
if word['startOffset'] < start:
continue
if word.get('case') == 'not-found-in-audio':
continue
if in_ == -1:
in_ = word['start']
out_ = word['end']
if word['endOffset'] > end:
break
if 'end' in word:
out_ = word['end']
if word['endOffset'] == end:
break
data.append({
'in': in_, 'out': out_, 'value': block.replace('\r\n', '\n')
})
end += len(new_block)
return data
def align_text(txt, wav):
cmd = ['curl', '-s', '-F', 'audio=@' + wav, '-F', 'transcript=@%s' % txt,
'http://localhost:8765/transcriptions?async=false']
data = subprocess.check_output(cmd).decode()
return json.loads(data)
def update_subtitles():
import item.models
from annotation.tasks import add_annotations
load_subs()
for i in item.models.Item.objects.filter(data__type=['Voice Over']):
wav = i.files.filter(selected=True)[0].data.path
id = i.get('title').split('_')[0]
batch = i.get('batch')[0][5:].lower().replace('-', '_').replace(' ', '')
txt = '/srv/t_for_time/vo/%s_%s.txt' % (batch, id)
if os.path.exists(txt):
print(i, wav, txt)
subtitles = gentle2subtitles(align_text(txt, wav))
add_annotations({
'item': i.public_id,
'layer': 'subtitles',
'user': 'j',
'annotations': subtitles
})

1300
config.jsonc Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
[Unit]
Description=render to infinity and beyond
After=pandora.service
[Service]
Type=simple
Restart=always
User=pandora
Group=pandora
Nice=19
WorkingDirectory=/srv/pandora/pandora
ExecStart=/srv/pandora/pandora/manage.py infinity
[Install]
WantedBy=multi-user.target

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

@ -0,0 +1,124 @@
import json
import os
import re
from collections import defaultdict
from django.core.management.base import BaseCommand
from django.conf import settings
import item.models
import itemlist.models
from ...render import get_srt
def resolve_roman(s):
extra = re.compile('^\d+(.*?)$').findall(s)
if extra:
extra = extra[0].lower()
new = {
'i': '1', 'ii': '2', 'iii': '3', 'iv': '4', 'v': '5',
'vi': '6', 'vii': 7, 'viii': '8', 'ix': '9', 'x': '10'
}.get(extra, extra)
return s.replace(extra, new)
return s
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)
if qs.count() >= 1:
clip = {}
durations = []
for e in item.models.Item.objects.filter(data__title=i.data['title']):
if 'type' not in e.data:
print("ignoring invalid video %s (no type)" % e)
continue
if not e.files.filter(selected=True).exists():
continue
source = e.files.filter(selected=True)[0].data.path
ext = os.path.splitext(source)[1]
type_ = e.data['type'][0].lower()
target = os.path.join(prefix, type_, i.data['title'] + ext)
os.makedirs(os.path.dirname(target), exist_ok=True)
if os.path.islink(target):
os.unlink(target)
os.symlink(source, target)
clip[type_] = target
durations.append(e.files.filter(selected=True)[0].duration)
clip["duration"] = min(durations)
if not clip["duration"]:
print('!!', durations, clip)
continue
clip['tags'] = i.data.get('tags', [])
clip['editingtags'] = i.data.get('editingtags', [])
name = os.path.basename(clip['original'])
seqid = re.sub("Hotel Aporia_(\d+)", "S\\1_", name)
seqid = re.sub("Night March_(\d+)", "S\\1_", seqid)
seqid = re.sub("_(\d+)H_(\d+)", "_S\\1\\2_", seqid)
seqid = seqid.split('_')[:2]
seqid = [b[1:] if b[0] in ('B', 'S') else '0' for b in seqid]
seqid[1] = resolve_roman(seqid[1])
seqid[1] = ''.join([b for b in seqid[1] if b.isdigit()])
if not seqid[1]:
seqid[1] = '0'
try:
clip['seqid'] = int(''.join(['%06d' % int(b) for b in seqid]))
except:
print(name, seqid, 'failed')
raise
if "original" in clip and "foreground" in clip and "background" in clip:
clips.append(clip)
elif "original" in clip and "animation" in clip:
clips.append(clip)
else:
print("ignoring incomplete video", i)
with open(os.path.join(prefix, 'clips.json'), 'w') as fd:
json.dump(clips, fd, indent=2, ensure_ascii=False)
print("using", len(clips), "clips")
voice_over = defaultdict(dict)
for vo in item.models.Item.objects.filter(
data__type__contains="Voice Over",
):
fragment_id = int(vo.get('title').split('_')[0])
source = vo.files.filter(selected=True)[0]
batch = vo.get('batch')[0].replace('Text-', '')
src = source.data.path
target = os.path.join(prefix, 'voice_over', batch, '%s.wav' % fragment_id)
os.makedirs(os.path.dirname(target), exist_ok=True)
if os.path.islink(target):
os.unlink(target)
os.symlink(src, target)
subs = []
for sub in vo.annotations.filter(layer="subtitles", 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,
"subs": subs
}
with open(os.path.join(prefix, 'voice_over.json'), 'w') as fd:
json.dump(voice_over, fd, indent=2, ensure_ascii=False)

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

@ -0,0 +1,19 @@
import json
import os
import subprocess
from django.core.management.base import BaseCommand
from django.conf import settings
from ...render import render_infinity
class Command(BaseCommand):
help = 'render infinity'
def add_arguments(self, parser):
parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in')
parser.add_argument('--duration', action='store', dest='duration', default="3600", help='target duration of all fragments in seconds')
def handle(self, **options):
render_infinity(options)

View file

@ -0,0 +1,21 @@
import json
import os
import subprocess
from django.core.management.base import BaseCommand
from django.conf import settings
from ...render import render_all
class Command(BaseCommand):
help = 'genrate kdenlive porject and render'
def add_arguments(self, parser):
parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in')
parser.add_argument('--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')
def handle(self, **options):
render_all(options)

View file

@ -0,0 +1,20 @@
import json
import os
import subprocess
from django.core.management.base import BaseCommand
from django.conf import settings
from ...render import update_subtitles
class Command(BaseCommand):
help = 'genrate kdenlive porject and render'
def add_arguments(self, parser):
parser.add_argument('--prefix', action='store', dest='prefix', default="/srv/t_for_time", help='prefix to build clips in')
parser.add_argument('--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)

18
pi.py Normal file
View file

@ -0,0 +1,18 @@
from mpmath import mp
class random(object):
def __init__(self, offset=0):
self.position = offset
mp.dps = 10000 + offset
self.PI = str(mp.pi).replace('.', '')
self.numbers = list(map(int, self.PI[offset:]))
def __call__(self):
if not self.numbers:
offset = mp.dps
mp.dps += 1000
self.PI = str(mp.pi).replace('.', '')
self.numbers = list(map(int, self.PI[offset:]))
self.position += 1
return self.numbers.pop(0)

1
player/check-overview Executable file
View file

@ -0,0 +1 @@
dbus-send --session --dest=org.gnome.Shell --print-reply /org/gnome/Shell org.freedesktop.DBus.Properties.Get string:org.gnome.Shell string:OverviewActive

3
player/disable-overview Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
dbus-send --session --dest=org.gnome.Shell --type=method_call /org/gnome/Shell org.freedesktop.DBus.Properties.Set string:org.gnome.Shell string:OverviewActive variant:boolean:false

8
player/play-back.desktop Normal file
View file

@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Exec=/srv/pandora/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
Name=t-for-time
Comment=

View file

@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Exec=/srv/pandora/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
Name=t-for-time
Comment=

View file

@ -0,0 +1,13 @@
[Unit]
Description=player
After=gnome-session.target
Wants=network-online.target
[Service]
Type=simple
Restart=on-failure
KillSignal=SIGINT
ExecStart=/srv/pandora/t_for_time/player/player.py --mode peer --playlist /srv/t_for_time/render/back.m3u
[Install]
WantedBy=graphical-session.target

View file

@ -0,0 +1,12 @@
[Unit]
Description=player
After=gnome-session.target network-online.target
[Service]
Type=simple
Restart=on-failure
KillSignal=SIGINT
ExecStart=/srv/pandora/t_for_time/player/player.py --mode main --playlist /srv/t_for_time/render/front.m3u
[Install]
WantedBy=graphical-session.target

449
player/player.py Executable file
View file

@ -0,0 +1,449 @@
#!/usr/bin/python3
import argparse
import collections
import json
import os
import socket
import time
from threading import Thread
from datetime import datetime
import ox
import mpv
import logging
logger = logging.getLogger('t_for_time')
SYNC_TOLERANCE = 0.05
SYNC_GRACE_TIME = 5
SYNC_JUMP_AHEAD = 1
PORT = 9067
DEBUG = False
CONFIG = {
"font": "Menlo",
"font_size": 30,
"font_border": 4,
"sub_border_color": "0.0/0.0/0.0/0.75",
"sub_margin": 2 * 36 + 6,
"sub_spacing": 0,
"vf": None,
"sync_group": None,
}
def hide_gnome_overview():
import dbus
bus = dbus.SessionBus()
shell = bus.get_object('org.gnome.Shell', '/org/gnome/Shell')
props = dbus.Interface(shell, 'org.freedesktop.DBus.Properties')
props.Set('org.gnome.Shell', 'OverviewActive', False)
def mpv_log(loglevel, component, message):
logger.info('[{}] {}: {}'.format(loglevel, component, message))
class Main:
playlist_current_pos = -1
time_pos = -1
class Sync(Thread):
active = True
is_main = True
is_paused = False
ready = False
destination = "255.255.255.255"
reload_check = None
_pos = None
_tick = 0
need_to_sync = False
def __init__(self, *args, **kwargs):
self.is_main = kwargs.get('mode', 'main') == 'main'
self.start_at_hour = kwargs.get("hour", False)
self.sock = self.init_socket()
self.main = Main()
if self.is_main:
self.socket_enable_broadcast()
if kwargs.get("sax"):
self.sax = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True,
)
self.sax.loop_file = True
self.sax.play("/srv/t_for_time/render/Saxophone-5.1.mp4")
else:
self.sax = None
if mpv.MPV_VERSION >= (2, 2):
self.mpv = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True,
sub_font_size=CONFIG["font_size"], sub_font=CONFIG["font"],
sub_border_size=CONFIG["font_border"],
sub_border_color=CONFIG["sub_border_color"],
sub_margin_y=CONFIG["sub_margin"],
sub_ass_line_spacing=CONFIG["sub_spacing"],
)
else:
self.mpv = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True,
sub_text_font_size=CONFIG["font_size"], sub_text_font=CONFIG["font"],
sub_border_size=CONFIG["font_border"],
sub_border_color=CONFIG["sub_border_color"],
sub_margin_y=CONFIG["sub_margin"],
sub_ass_line_spacing=CONFIG["sub_spacing"],
)
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)
logger.error("loaded paylist: %s", self.playlist)
logger.debug("current playlist: %s", json.dumps(self.mpv.playlist, indent=2))
self.deviations = collections.deque(maxlen=10)
if not self.is_main:
self.mpv.pause = False
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()
def run(self):
while self.active:
if self.is_main:
time.sleep(0.5)
else:
if self.need_to_sync:
self.sync_to_main()
self.deviations = collections.deque(maxlen=10)
self.need_to_sync = False
else:
self.read_position_main()
self.reload_playlist()
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:
self.sock.close()
self.sock = None
def time_pos_cb(self, pos, *args, **kwargs):
self._tick = time.time()
if self.is_main:
self.send_position_local()
elif self.ready:
self.adjust_position()
if self._pos != self.mpv.playlist_current_pos:
self._pos = self.mpv.playlist_current_pos
self.deviations = collections.deque(maxlen=10)
self.need_to_sync = False
try:
track = self.mpv.playlist[self._pos]
logger.error("%s %s", datetime.now(), track["filename"])
except:
pass
def reload_playlist(self):
if not self.reload_check:
self.reload_check = time.time()
if time.time() - self.reload_check > 5:
self.reload_check = time.time()
playlist_mtime = os.stat(self.playlist).st_mtime
if self.playlist_mtime != playlist_mtime:
self.playlist_mtime = playlist_mtime
#self.mpv.loadlist(self.playlist)
with open(self.playlist) as fd:
items = fd.read().strip().split('\n')
base = os.path.dirname(self.playlist)
items = [os.path.join(base, item) for item in items]
current_items = self.mpv.playlist_filenames
for filename in items:
if filename not in current_items:
self.mpv.playlist_append(filename)
logger.error("add: %s", filename)
remove = []
for filename in current_items:
if filename not in items:
remove.append(filename)
for filename in remove:
for idx, item in enumerate(self.mpv.playlist):
if item["filename"] == filename:
logger.error("remove: %s %s", idx, filename)
self.mpv.playlist_remove(idx)
break
for idx, filename in enumerate(items):
current_idx = self.mpv.playlist_filenames.index(filename)
if idx != current_idx:
logger.error("move item %s %s -> %s", filename, current_idx, idx)
self.mpv.playlist_move(current_idx, idx)
logger.error("reloaded paylist: %s", self.playlist)
logger.debug("current playlist: %s", json.dumps(self.mpv.playlist, indent=2))
def init_socket(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", PORT))
return sock
#
# main specific
#
def socket_enable_broadcast(self):
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.sock.connect((self.destination, PORT))
def send_position_local(self):
if not self.active:
return
try:
msg = (
"%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:
self.sock.send(msg)
except socket.error as e:
logger.error("send failed: %s", e)
def send_playback_state(self):
state = 'pause' if self.mpv.pause else 'play'
msg = ("%s -1" % state).encode()
try:
self.sock.send(msg)
except socket.error as e:
logger.error("send failed: %s", e)
#
# follower specific
#
_last_ping = None
def read_position_main(self):
self.sock.settimeout(5)
while True:
try:
data = self.sock.recvfrom(1024)[0].decode().split(" ", 1)
except socket.timeout:
if self._last_ping != "pause":
logger.error("failed to receive data from main")
return
except OSError:
logger.error("socket closed")
return
if CONFIG.get("sync_group"):
if data[0] == str(CONFIG["sync_group"]):
data = data[1].split(" ", 1)
break
else:
break
self._last_ping = data[0]
if data[0] == "pause":
self.is_paused = True
self.mpv.pause = True
elif data[0] == "play":
self.is_paused = False
self._tick = 0
self.mpv.pause = False
else:
self.main.time_pos = float(data[0])
self.main.playlist_current_pos = int(data[1])
def adjust_position(self):
if self.mpv.time_pos is not None:
try:
deviation = self.main.time_pos - self.mpv.time_pos
except:
return
self.deviations.append(deviation)
median_deviation = self.median(list(self.deviations))
frames = deviation / 0.04
median_frames = median_deviation / 0.04
if abs(deviation) <= 0.04 and self.mpv.speed != 1.0:
self.mpv.speed = 1.0
logger.error(
'%0.05f back to normal speed %0.05f (%d) median %0.05f (%d) -> %s' % (self.mpv.time_pos, deviation, frames, median_deviation, median_frames, self.mpv.speed)
)
if time.time() - self.last_sync > SYNC_GRACE_TIME and abs(median_deviation) > SYNC_TOLERANCE:
if abs(median_deviation) < 1:
step = 0.02
if median_deviation > 0:
self.mpv.speed += step
else:
self.mpv.speed -= step
logger.error(
'%0.05f need to adjust speed %0.05f (%d) median %0.05f (%d) -> %s' % (self.mpv.time_pos, deviation, frames, median_deviation, median_frames, self.mpv.speed)
)
self.need_to_sync = False
self.deviations = collections.deque(maxlen=10)
self.last_sync = time.time()
elif self.mpv.time_pos > 2 and not self.need_to_sync:
logger.error(
'%0.05f need to sync %0.05f (%d) median %0.05f (%d)' % (self.mpv.time_pos, deviation, frames, median_deviation, median_frames)
)
self.need_to_sync = True
def median(self, lst):
quotient, remainder = divmod(len(lst), 2)
if remainder:
return sorted(lst)[quotient]
return float(sum(sorted(lst)[quotient - 1:quotient + 1]) / 2.0)
def sync_to_main(self):
logger.error('sync to main')
self.read_position_main()
#print(self.main.playlist_current_pos)
if self.main.playlist_current_pos != self.mpv.playlist_current_pos:
self.mpv.playlist_play_index(self.main.playlist_current_pos)
self.mpv.pause = False
self.mpv.wait_until_playing()
try:
track = self.mpv.playlist[self.mpv.playlist_current_pos]
logger.error("%s %s", datetime.now(), track["filename"])
except:
pass
self.mpv.pause = True
self.mpv.speed = 1
pos = self.main.time_pos + SYNC_JUMP_AHEAD
#print(pos, self.mpv.playlist_current_pos, self.mpv.time_pos)
self.mpv.seek(pos, 'absolute', 'exact')
time.sleep(0.1)
self.read_position_main()
sync_timer = time.time() # - 10 * 0.04
deviation = self.main.time_pos - self.mpv.time_pos
while self.active:
#print(deviation, abs(deviation) - (time.time() - sync_timer))
if abs(deviation) - (time.time() - sync_timer) < 0:
self.mpv.pause = False
try:
track = self.mpv.playlist[self.mpv.playlist_current_pos]
logger.error("%s %s %s", datetime.now(), track["filename"], pos)
except:
pass
break
self.last_sync = time.time()
def main():
prefix = os.path.expanduser('~/Videos/t_for_time')
parser = argparse.ArgumentParser(description='t_for_time sync player')
parser.add_argument('--mode', help='peer or main', default="peer")
parser.add_argument('--playlist', default='/srv/t_for_time/render/128/front.m3u', help="m3u")
parser.add_argument('--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, hour=args.hour, sax=args.sax)
while player.active:
try:
player.mpv.wait_for_playback()
except:
break
player.stop()
if __name__ == "__main__":
try:
hide_gnome_overview()
except:
pass
main()

View file

@ -0,0 +1,8 @@
[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=

11
player/saxophone.service Normal file
View file

@ -0,0 +1,11 @@
[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

717
render.py Normal file
View file

@ -0,0 +1,717 @@
#!/usr/bin/python3
from collections import defaultdict
from glob import glob
import json
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
import ox
from .pi import random
from .render_kdenlive import KDEnliveProject, _CACHE
def random_int(seq, length):
n = n_ = length - 1
#print('len', n)
if n == 0:
return n
r = seq() / 9 * 10
base = 10
while n > 10:
n /= 10
r += seq() / 9 * 10
base += 10
r = int(round(n_ * r / base))
return r
def random_choice(seq, items, pop=False):
n = random_int(seq, len(items))
if pop:
return items.pop(n)
return items[n]
def chance(seq, chance):
return (seq() / 10) < chance
def get_clip_by_seqid(clips, seqid):
selected = None
for i, clip in enumerate(clips):
if clip['seqid'] == seqid:
selected = i
break
if selected is not None:
return clips.pop(i)
return None
def write_if_new(path, data, mode=''):
read_mode = 'r' + mode
write_mode = 'w' + mode
if os.path.exists(path):
with open(path, read_mode) as fd:
old = fd.read()
else:
old = ""
is_new = data != old
if path.endswith(".kdenlive"):
is_new = re.sub('\{.{36}\}', '', data) != re.sub('\{.{36}\}', '', old)
if is_new:
with open(path, write_mode) as fd:
fd.write(data)
def compose(clips, target=150, base=1024, voice_over=None):
length = 0
scene = {
'front': {
'V1': [],
'V2': [],
},
'back': {
'V1': [],
'V2': [],
},
'audio-back': {
'A1': [],
},
'audio-center': {
'A1': [],
},
'audio-front': {
'A1': [],
'A2': [],
'A3': [],
'A4': [],
},
'audio-rear': {
'A1': [],
'A2': [],
'A3': [],
'A4': [],
},
}
all_clips = clips.copy()
seq = random(10000 + base * 1000)
used = []
voice_overs = []
if voice_over:
vo_keys = list(sorted(voice_over))
if chance(seq, 0.5):
vo_key = vo_keys[random_int(seq, len(vo_keys))]
voice_overs.append(voice_over[vo_key])
elif len(vo_keys) >= 2:
vo1 = vo_keys.pop(random_int(seq, len(vo_keys)))
vo2 = vo_keys.pop(random_int(seq, len(vo_keys)))
voice_overs.append(voice_over[vo1])
if voice_over[vo1]["duration"] + voice_over[vo2]["duration"] < target:
print("adding second vo")
voice_overs.append(voice_over[vo2])
print("vo:", [x['src'] for x in voice_overs], list(sorted(voice_over)))
vo_min = sum([vo['duration'] for vo in voice_overs])
sub_offset = 0
if vo_min > target:
target = vo_min
elif vo_min < target:
offset = (target - vo_min) / 2
scene['audio-center']['A1'].append({
'blank': True,
'duration': offset
})
scene['audio-rear']['A1'].append({
'blank': True,
'duration': offset
})
vo_min += offset
sub_offset = offset
subs = []
for vo in voice_overs:
voc = vo.copy()
a, b = '-11', '-3'
if 'Whispered' in voc['src']:
a, b = '-8', '0'
elif 'Read' in voc['src']:
a, b = '-7.75', '0.25'
elif 'Free' in voc['src']:
a, b = '-8.8', '-0.8'
elif 'Ashley' in voc['src']:
a, b = '-9.5', '-1.50'
elif 'Melody' in voc['src']:
a, b = '-5.25', '-0.25'
voc['filter'] = {'volume': a}
scene['audio-center']['A1'].append(voc)
vo_low = vo.copy()
vo_low['filter'] = {'volume': b}
scene['audio-rear']['A1'].append(vo_low)
for sub in voc.get("subs", []):
sub = sub.copy()
sub["in"] += sub_offset
sub["out"] += sub_offset
subs.append(sub)
sub_offset += voc["duration"]
if subs:
scene["subtitles"] = subs
clip = None
while target - length > 0 and clips:
# coin flip which site is visible (50% chance)
if length:
remaining = target - length
remaining = remaining * 1.05 # allow for max of 10% over time
clips_ = [c for c in clips if c['duration'] <= remaining]
if clips_:
clips = clips_
if clip:
if chance(seq, 0.5):
next_seqid = clip['seqid'] + 1
clip = get_clip_by_seqid(clips, next_seqid)
else:
clip = None
if not clip:
clip = random_choice(seq, clips, True)
if not clips:
print("not enough clips, need to reset")
clips = [c for c in all_clips if c != clip and c not in used]
if not clips:
print("not enough clips, also consider used")
clips = [c for c in all_clips if c != clip]
if not clips:
print("not enough clips, also consider last clip")
clips = all_clips.copy()
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']
if "foreground" not in clip and "animation" in clip:
fg = clip['animation']
transparancy = 1
else:
fg = clip['foreground']
if 'animation' in clip and chance(seq, 0.15):
fg = clip['animation']
transparancy = 1
else:
if 'foreground2' in clip:
if 'foreground3' in clip:
n = seq()
if n <= 3: # 0,1,2,3
clip['foreground']
elif n <= 6: # 4,5,6
clip['foreground2']
else: # 7,8,9
clip['foreground3']
elif chance(seq, 0.5):
fg = clip['foreground2']
transparancy = seq() / 9
transparancy = 1
if 'foley' in clip:
foley = clip['foley']
else:
foley = fg
scene['front']['V2'].append({
'duration': clip['duration'],
'src': fg,
"filter": {
'transparency': transparancy,
}
})
transparency = seq() / 9
# 50% of time no transparancy of foregroudnd layer
# 50% some transparancy, 25%, 50%, 75% levels of transparancy
transparancy = 1
# coin flip which site is visible (50% chance)
#if chance(seq, 0.5):
if chance(seq, 0.8):
transparency_front = transparency
transparency_back = 0
else:
transparency_back = random_choice(seq, [0.25, 0.5, 0.75, 1])
transparency_front = 0
transparency_original = seq() / 9
transparency_original = 1
if "background" in clip:
scene['front']['V1'].append({
'duration': clip['duration'],
'src': clip['background'],
"filter": {
'transparency': transparency_front
}
})
scene['back']['V2'].append({
'duration': clip['duration'],
'src': clip['background'],
"filter": {
'transparency': transparency_back
}
})
else:
scene['front']['V1'].append({
'duration': clip['duration'],
'src': clip['animation'],
"filter": {
'transparency': 0,
}
})
scene['back']['V2'].append({
'duration': clip['duration'],
'src': clip['original'],
"filter": {
'transparency': 0,
}
})
scene['back']['V1'].append({
'duration': clip['duration'],
'src': clip['original'],
"filter": {
'transparency': transparency_original,
}
})
# 50 % chance to blur original from 0 to 30
if chance(seq, 0.5):
blur = seq() * 3
if blur:
scene['back']['V1'][-1]['filter']['blur'] = blur
scene['audio-back']['A1'].append({
'duration': clip['duration'],
'src': clip['original'],
'filter': {'volume': '-8.2'},
})
# TBD: Foley
cf_volume = '-2.5'
scene['audio-front']['A2'].append({
'duration': clip['duration'],
'src': foley,
'filter': {'volume': cf_volume},
})
scene['audio-rear']['A2'].append({
'duration': clip['duration'],
'src': foley,
'filter': {'volume': cf_volume},
})
used.append(clip)
print("scene duration %0.3f (target: %0.3f, vo_min: %0.3f)" % (length, target, vo_min))
return scene, used
def get_scene_duration(scene):
duration = 0
for key, value in scene.items():
for name, clips in value.items():
for clip in clips:
duration += clip['duration']
return duration
def get_offset_duration(prefix):
duration = 0
for root, folders, files in os.walk(prefix):
for f in files:
if f == 'scene.json':
path = os.path.join(root, f)
scene = json.load(open(path))
duration += get_scene_duration(scene)
return duration
def render(root, scene, prefix=''):
fps = 24
files = []
scene_duration = int(get_scene_duration(scene) * 24)
for timeline, data in scene.items():
if timeline == "subtitles":
path = os.path.join(root, prefix + "front.srt")
data = fix_overlaps(data)
srt = ox.srt.encode(data)
write_if_new(path, srt, 'b')
continue
#print(timeline)
project = KDEnliveProject(root)
tracks = []
track_durations = {}
for track, clips in data.items():
#print(track)
for clip in clips:
project.append_clip(track, clip)
track_durations[track] = int(sum([c['duration'] for c in clips]) * 24)
if timeline.startswith('audio-'):
track_duration = project.get_duration()
delta = scene_duration - track_duration
if delta > 0:
for track in track_durations:
if track_durations[track] == track_duration:
project.append_clip(track, {'blank': True, "duration": delta/24})
break
path = os.path.join(root, prefix + "%s.kdenlive" % timeline)
project_xml = project.to_xml()
write_if_new(path, project_xml)
files.append(path)
return files
def get_fragments(clips, voice_over, prefix):
import itemlist.models
import item.models
fragments = []
for l in itemlist.models.List.objects.filter(status='featured').order_by('name'):
if l.name.split(' ')[0].isdigit():
fragment = {
'name': l.name,
'tags': [],
'anti-tags': [],
'description': l.description
}
for con in l.query['conditions']:
if "conditions" in con:
for sub in con["conditions"]:
if sub['key'] == "tags" and sub['operator'] == '==':
fragment['tags'].append(sub['value'])
elif sub['key'] == "tags" and sub['operator'] == '!=':
fragment['tags'].append(sub['value'])
else:
print(l.name, 'unknown sub condition', sub)
elif con.get('key') == "tags" and con['operator'] == '==':
fragment['tags'].append(con['value'])
elif con.get('key') == "tags" and con['operator'] == '!=':
fragment['anti-tags'].append(con['value'])
fragment["id"] = int(fragment['name'].split(' ')[0])
originals = []
for i in l.get_items(l.user):
orig = i.files.filter(selected=True).first()
if orig:
ext = os.path.splitext(orig.data.path)[1]
type_ = i.data['type'][0].lower()
target = os.path.join(prefix, type_, i.data['title'] + ext)
originals.append(target)
fragment['clips'] = []
for clip in clips:
#if set(clip['tags']) & set(fragment['tags']) and not set(clip['tags']) & set(fragment['anti-tags']):
if clip['original'] in originals:
fragment['clips'].append(clip)
fragment["voice_over"] = voice_over.get(str(fragment["id"]), {})
fragments.append(fragment)
fragments.sort(key=lambda f: ox.sort_string(f['name']))
return fragments
def render_all(options):
prefix = options['prefix']
duration = int(options['duration'])
base = int(options['offset'])
_cache = os.path.join(prefix, "cache.json")
if os.path.exists(_cache):
with open(_cache) as fd:
_CACHE.update(json.load(fd))
with open(os.path.join(prefix, "clips.json")) as fd:
clips = json.load(fd)
with open(os.path.join(prefix, "voice_over.json")) as fd:
voice_over = json.load(fd)
fragments = get_fragments(clips, voice_over, prefix)
with open(os.path.join(prefix, "fragments.json"), "w") as fd:
json.dump(fragments, fd, indent=2, ensure_ascii=False)
position = target_position = 0
target = fragment_target = duration / len(fragments)
base_prefix = os.path.join(prefix, 'render', str(base))
clips_used = []
stats = defaultdict(lambda: 0)
fragment_base = base
for fragment in fragments:
fragment_base += 1
fragment_id = int(fragment['name'].split(' ')[0])
name = fragment['name'].replace(' ', '_')
if fragment_id < 10:
name = '0' + name
if not fragment['clips']:
print("skipping empty fragment", name)
continue
fragment_prefix = os.path.join(base_prefix, name)
os.makedirs(fragment_prefix, exist_ok=True)
fragment_clips = fragment['clips']
unused_fragment_clips = [c for c in fragment_clips if c not in clips_used]
print('fragment clips', len(fragment_clips), 'unused', len(unused_fragment_clips))
scene, used = compose(unused_fragment_clips, target=target, base=fragment_base, voice_over=fragment['voice_over'])
clips_used += used
scene_duration = get_scene_duration(scene)
print("%s %6.3f -> %6.3f (%6.3f)" % (name, target, scene_duration, fragment_target))
src = [a for a in scene['audio-rear']['A1'] if 'src' in a][0]['src']
stats[src.split('/')[-2]] += 1
position += scene_duration
target_position += fragment_target
if position > target_position:
target = fragment_target - (position-target_position)
print("adjusting target duration for next fragment: %6.3f -> %6.3f" % (fragment_target, target))
elif position < target_position:
target = target + 0.1 * fragment_target
timelines = render(prefix, scene, fragment_prefix[len(prefix) + 1:] + '/')
scene_json = json.dumps(scene, indent=2, ensure_ascii=False)
write_if_new(os.path.join(fragment_prefix, 'scene.json'), scene_json)
if not options['no_video']:
for timeline in timelines:
print(timeline)
ext = '.mp4'
if '/audio' in timeline:
ext = '.wav'
cmd = [
'xvfb-run', '-a',
'melt', timeline,
'-quiet',
'-consumer', 'avformat:%s' % timeline.replace('.kdenlive', ext),
]
if ext == '.wav':
cmd += ['vn=1']
else:
#if not timeline.endswith("back.kdenlive"):
cmd += ['an=1']
cmd += ['vcodec=libx264', 'x264opts=keyint=1', 'crf=15']
subprocess.call(cmd)
if ext == '.wav' and timeline.endswith('audio.kdenlive'):
cmd = [
'ffmpeg', '-y',
'-nostats', '-loglevel', 'error',
'-i',
timeline.replace('.kdenlive', ext),
timeline.replace('.kdenlive', '.mp4')
]
subprocess.call(cmd)
os.unlink(timeline.replace('.kdenlive', ext))
fragment_prefix = Path(fragment_prefix)
cmds = []
for src, out1, out2 in (
("audio-front.wav", "fl.wav", "fr.wav"),
("audio-center.wav", "fc.wav", "lfe.wav"),
("audio-rear.wav", "bl.wav", "br.wav"),
):
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-i", fragment_prefix / src,
"-filter_complex",
"[0:0]pan=1|c0=c0[left]; [0:0]pan=1|c0=c1[right]",
"-map", "[left]", fragment_prefix / out1,
"-map", "[right]", fragment_prefix / out2,
])
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-i", fragment_prefix / "fl.wav",
"-i", fragment_prefix / "fr.wav",
"-i", fragment_prefix / "fc.wav",
"-i", fragment_prefix / "lfe.wav",
"-i", fragment_prefix / "bl.wav",
"-i", fragment_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", fragment_prefix / "audio-5.1.mp4"
])
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-i", fragment_prefix / "front.mp4",
"-i", fragment_prefix / "audio-5.1.mp4",
"-c", "copy",
fragment_prefix / "front-5.1.mp4",
])
cmds.append([
"ffmpeg", "-y",
"-nostats", "-loglevel", "error",
"-i", fragment_prefix / "back.mp4",
"-i", fragment_prefix / "audio-back.wav",
"-c:v", "copy",
fragment_prefix / "back-audio.mp4",
])
for cmd in cmds:
#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(fragment_prefix / a))['duration']
duration_b = ox.avinfo(str(fragment_prefix / b))['duration']
if duration_a != duration_b:
print('!!', duration_a, fragment_prefix / a)
print('!!', duration_b, fragment_prefix / b)
sys.exit(-1)
shutil.move(fragment_prefix / "back-audio.mp4", fragment_prefix / "back.mp4")
shutil.move(fragment_prefix / "front-5.1.mp4", fragment_prefix / "front.mp4")
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 = fragment_prefix / fn
if os.path.exists(fn):
os.unlink(fn)
print("Duration - Target: %s Actual: %s" % (target_position, position))
print(json.dumps(dict(stats), sort_keys=True, indent=2))
with open(_cache, "w") as fd:
json.dump(_CACHE, fd)
def add_translations(sub, lang):
value = sub.value.replace('<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').strip()
if lang:
sdata['value'] = add_translations(sub, lang)
if offset:
sdata["in"] += offset
sdata["out"] += offset
return sdata
def fix_overlaps(data):
previous = None
for sub in data:
if previous is None:
previous = sub
else:
if sub['in'] < previous['out']:
previous['out'] = sub['in'] - 0.001
previous = sub
return data
def update_subtitles(options):
import item.models
prefix = Path(options['prefix'])
base = int(options['offset'])
lang = options["lang"]
if lang and "," in lang:
lang = lang.split(',')
if isinstance(lang, list):
tlang = lang[1:]
lang = lang[0]
else:
tlang = None
if lang == "en":
lang = None
_cache = os.path.join(prefix, "cache.json")
if os.path.exists(_cache):
with open(_cache) as fd:
_CACHE.update(json.load(fd))
base_prefix = prefix / 'render' / str(base)
for folder in os.listdir(base_prefix):
folder = base_prefix / folder
scene_json = folder / "scene.json"
if not os.path.exists(scene_json):
continue
with open(scene_json) as fd:
scene = json.load(fd)
offset = 0
subs = []
for clip in scene['audio-center']['A1']:
if not clip.get("blank"):
batch, fragment_id = clip['src'].replace('.wav', '').split('/')[-2:]
vo = item.models.Item.objects.filter(data__batch__icontains=batch, data__title__startswith=fragment_id + '_').first()
if vo:
#print("%s => %s %s" % (clip['src'], vo, vo.get('batch')))
for sub in vo.annotations.filter(layer="subtitles").filter(languages=lang).exclude(value="").order_by("start"):
sdata = get_srt(sub, offset, tlang)
subs.append(sdata)
else:
print("could not find vo for %s" % clip['src'])
offset += clip['duration']
path = folder / "front.srt"
data = fix_overlaps(subs)
srt = ox.srt.encode(subs)
write_if_new(str(path), srt, 'b')
def update_m3u(render_prefix, exclude=[]):
files = ox.sorted_strings(glob(render_prefix + "*/*/back.mp4"))
for ex in exclude:
files = [f for f in files if not f.startswith(ex + "/")]
back_m3u = "\n".join(files)
back_m3u = back_m3u.replace(render_prefix, "")
front_m3u = back_m3u.replace("back.mp4", "front.mp4")
back_m3u_f = render_prefix + "back.m3u"
front_m3u_f = render_prefix + "front.m3u"
with open(back_m3u_f + "_", "w") as fd:
fd.write(back_m3u)
with open(front_m3u_f + "_", "w") as fd:
fd.write(front_m3u)
shutil.move(front_m3u_f + "_", front_m3u_f)
cmd = ["scp", front_m3u_f, "front:" + front_m3u_f]
subprocess.check_call(cmd)
shutil.move(back_m3u_f + "_", back_m3u_f)
def render_infinity(options):
prefix = options['prefix']
duration = int(options['duration'])
state_f = os.path.join(prefix, "infinity.json")
if os.path.exists(state_f):
with open(state_f) as fd:
state = json.load(fd)
else:
state = {
"offset": 100,
"max-items": 30,
"no_video": False,
}
for key in ("prefix", "duration"):
state[key] = options[key]
while True:
render_prefix = state["prefix"] + "/render/"
current = [
f for f in os.listdir(render_prefix)
if f.isdigit() and os.path.isdir(render_prefix + f) and state["offset"] > int(f) >= 100
]
if len(current) > state["max-items"]:
current = ox.sorted_strings(current)
remove = current[:-state["max-items"]]
update_m3u(render_prefix, exclude=remove)
for folder in remove:
folder = render_prefix + folder
print("remove", folder)
shutil.rmtree(folder)
cmd = ["ssh", "front", "rm", "-rf", folder]
#print(cmd)
subprocess.check_call(cmd)
render_all(state)
path = "%s%s/" % (render_prefix, state["offset"])
cmd = ['rsync', '-a', path, "front:" + path]
subprocess.check_call(cmd)
update_m3u(render_prefix)
state["offset"] += 1
with open(state_f + "~", "w") as fd:
json.dump(state, fd, indent=2)
shutil.move(state_f + "~", state_f)

648
render_kdenlive.py Normal file
View file

@ -0,0 +1,648 @@
#!/usr/bin/python3
from collections import defaultdict
import subprocess
import lxml.etree
import uuid
import os
_CACHE = {}
_IDS = defaultdict(int)
def get_propery(element, name):
return element.xpath('property[@name="%s"]' % name)[0].text
def melt_xml(file):
out = None
real_path = os.path.realpath(file)
if file in _CACHE and isinstance(_CACHE[file], list):
ts, out = _CACHE[file]
if os.stat(real_path).st_mtime != ts:
out = None
if not out:
out = subprocess.check_output(['melt', file, '-consumer', 'xml']).decode()
_CACHE[file] = [os.stat(real_path).st_mtime, out]
return out
class KDEnliveProject:
def to_xml(self):
track = self._main_tractor.xpath(".//track")[0]
duration = self.get_duration()
values = {
"in": "0",
"out": str(duration - 1)
}
for key, value in values.items():
track.attrib[key] = value
self._sequence.attrib[key] = value
self._main_tractor.attrib[key] = value
self._audio_tractor.attrib[key] = value
self._tree.remove(self._sequence)
self._tree.append(self._sequence)
self._tree.remove(self._main_bin)
self._tree.append(self._main_bin)
self._tree.remove(self._main_tractor)
self._tree.append(self._main_tractor)
xml = lxml.etree.tostring(self._tree, pretty_print=True).decode()
xml = xml.replace('><', '>\n<')
return "<?xml version='1.0' encoding='utf-8'?>\n" + xml
def __init__(
self, root,
width="1920", height="1080",
display_aspect_num="16", display_aspect_den="9",
frame_rate_num="24", frame_rate_den="1"
):
self._duration = defaultdict(int)
self._counters = defaultdict(int)
self._uuid = '{%s}' % str(uuid.uuid1())
self._width = int(width)
self._height = int(height)
self._fps = int(frame_rate_num) / int(frame_rate_den)
self._tree = self.get_element("mlt", attrib={
"LC_NUMERIC": "C",
"producer": "main_bin",
"version": "7.18.0",
"root": root
}, children=[
self.get_element("profile", attrib={
"frame_rate_num": str(frame_rate_num),
"frame_rate_den": str(frame_rate_den),
"display_aspect_den": str(display_aspect_den),
"display_aspect_num": str(display_aspect_num),
"colorspace": "601",
"progressive": "1",
"description": "%sx%s %0.2ffps" % (self._width, self._height, self._fps),
"width": str(width),
"height": str(height),
"sample_aspect_num": "1",
"sample_aspect_den": "1"
}),
p0 := self.get_element("producer", attrib={
"in": "0",
"out": "2147483647"
}, children=[
["length", "2147483647"],
["eof", "continue"],
["resource", "black"],
["aspect_ratio", "1"],
["mlt_service", "color"],
["kdenlive:playlistid", "black_track"],
["mlt_image_format", "rgba"],
["set.test_audio", "0"],
]),
a4 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a4e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a4 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a4.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a4e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
a3 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a3e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a3 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a3.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a3e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
a2 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a2e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a2 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a2.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a2e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
a1 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a1e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a1 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a1.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a1e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
v1 := self.get_element("playlist", children=[
]),
v1e := self.get_element("playlist", children=[
]),
t2 := self.get_element("tractor", attrib={
"in": "00:00:00.000",
"out": "00:00:25.333"
}, children=[
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
["kdenlive:locked_track", None],
self.get_element("track", attrib={"hide": "audio", "producer": v1.attrib["id"]}),
self.get_element("track", attrib={"hide": "audio", "producer": v1e.attrib["id"]}),
]),
v2 := self.get_element("playlist", children=[
]),
v2e := self.get_element("playlist", children=[
]),
t3 := self.get_element("tractor", attrib={
"in": "00:00:00.000"
}, children=[
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
["kdenlive:locked_track", None],
self.get_element("track", attrib={"hide": "audio", "producer": v2.attrib["id"]}),
self.get_element("track", attrib={"hide": "audio", "producer": v2e.attrib["id"]}),
]),
sequence := self.get_element("tractor", [
["kdenlive:uuid", self._uuid],
["kdenlive:clipname", "Sequence 1"],
["kdenlive:sequenceproperties.hasAudio", "1"],
["kdenlive:sequenceproperties.hasVideo", "1"],
["kdenlive:sequenceproperties.activeTrack", "2"],
["kdenlive:sequenceproperties.tracksCount", "4"],
["kdenlive:sequenceproperties.documentuuid", self._uuid],
["kdenlive:duration", "00:00:25:09"],
["kdenlive:maxduration", "872"],
["kdenlive:producer_type", "17"],
["kdenlive:id", self.get_counter("kdenlive:id")],
["kdenlive:clip_type", "0"],
["kdenlive:folderid", "2"],
["kdenlive:sequenceproperties.audioChannels", "2"],
["kdenlive:sequenceproperties.audioTarget", "1"],
["kdenlive:sequenceproperties.tracks", "4"],
["kdenlive:sequenceproperties.verticalzoom", "1"],
["kdenlive:sequenceproperties.videoTarget", "2"],
["kdenlive:sequenceproperties.zonein", "0"],
["kdenlive:sequenceproperties.zoneout", "75"],
["kdenlive:sequenceproperties.zoom", "8"],
["kdenlive:sequenceproperties.groups", "[]"],
["kdenlive:sequenceproperties.guides", "[]"],
["kdenlive:sequenceproperties.position", "0"],
["kdenlive:sequenceproperties.scrollPos", "0"],
["kdenlive:sequenceproperties.disablepreview", "0"],
self.get_element("track", attrib={"producer": p0.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a4.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a3.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a2.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a1.attrib["id"]}),
self.get_element("track", attrib={"producer": t2.attrib["id"]}),
self.get_element("track", attrib={"producer": t3.attrib["id"]}),
self.get_element("transition", [
["a_track", "0"],
["b_track", "1"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "2"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "3"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "4"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "5"],
["compositing", "0"],
["distort", "0"],
["rotate_center", "0"],
["mlt_service", "qtblend"],
["kdenlive_id", "qtblend"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "6"],
["compositing", "0"],
["distort", "0"],
["rotate_center", "0"],
["mlt_service", "qtblend"],
["kdenlive_id", "qtblend"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
], {
"id": self._uuid
}),
main_bin := self.get_element("playlist", [
["kdenlive:folder.-1.2", "Sequences"],
["kdenlive:sequenceFolder", "2"],
["kdenlive:docproperties.kdenliveversion", "23.08.0"],
self.get_element("property", attrib={"name": "kdenlive:docproperties.previewextension"}),
self.get_element("property", attrib={"name": "kdenlive:docproperties.previewparameters"}),
["kdenlive:docproperties.seekOffset", "30000"],
["kdenlive:docproperties.uuid", self._uuid],
["kdenlive:docproperties.version", "1.1"],
["kdenlive:expandedFolders", None],
["kdenlive:binZoom", "4"],
self.get_element("property", attrib={"name": "kdenlive:documentnotes"}),
["kdenlive:docproperties.opensequences", self._uuid],
["kdenlive:docproperties.activetimeline", self._uuid],
["xml_retain", "1"],
self.get_element("entry", attrib={"producer": self._uuid, "in": "0", "out": "0"}),
], {
"id": "main_bin"
}),
t4 := self.get_element("tractor", [
["kdenlive:projectTractor", "1"],
self.get_element("track", attrib={"producer": self._uuid}),
])
])
self._sequence = sequence
self._main_bin = main_bin
self._main_tractor = t4
self._audio_tractor = t_a1
self._v1 = v1
self._v2 = v2
self._a1 = a1
self._a2 = a2
self._a3 = a3
self._a4 = a4
def get_counter(self, prefix):
self._counters[prefix] += 1
return str(self._counters[prefix] - 1)
def get_id(self, prefix):
return prefix + self.get_counter(prefix)
def get_chain(self, file, kdenlive_id=None):
out = melt_xml(file)
chain = lxml.etree.fromstring(out).xpath('producer')[0]
chain.tag = 'chain'
chain.attrib['id'] = self.get_id('chain')
# TBD
if kdenlive_id is None:
kdenlive_id = self.get_counter("kdenlive:id")
for name, value in [
("kdenlive:file_size", os.path.getsize(file)),
("kdenlive:clipname", None),
("kdenlive:clip_type", "0"),
("kdenlive:folderid", "-1"),
("kdenlive:id", kdenlive_id),
("set.test_audio", "0"),
("set.test_image", "0"),
("xml", "was here"),
]:
chain.append(
self.get_element(
"property",
attrib={"name": name},
text=str(value) if value is not None else None
)
)
mlt_service = chain.xpath('property[@name="mlt_service"]')[0]
mlt_service.text = "avformat-novalidate"
return chain
def get_duration(self):
if not self._duration:
return 0
return max(self._duration.values())
def get_element(self, tag, children=[], attrib={}, text=None):
element = lxml.etree.Element(tag)
if tag not in (
"blank",
"entry",
"mlt",
"profile",
"property",
"track",
) and "id" not in attrib:
element.attrib['id'] = self.get_id(tag)
if attrib:
for key, value in attrib.items():
element.attrib[key] = value
for child in children:
if isinstance(child, list) and len(child) == 2:
v = child[1]
if v is not None:
v = str(v)
child = self.get_element("property", attrib={"name": child[0]}, text=v)
if isinstance(child, dict):
child = self.get_element(**child)
elif isinstance(child, list):
child = self.get_element(*child)
element.append(child)
if text is not None:
element.text = text
return element
def get_filter(self, name, value):
if name == "transparency":
return [self.get_element("filter", [
["version", "0.9"],
["mlt_service", "frei0r.transparency"],
["kdenlive_id", "frei0r.transparency"],
["0", "00:00:00.000=%s" % value],
["kdenlive:collapsed", "0"],
])]
elif name == "blur":
return [self.get_element("filter", [
["mlt_service", "avfilter.avgblur"],
["kdenlive_id", "avfilter.avgblur"],
["av.sizeX", value],
["av.sizeY", value],
["planes", "7"],
["kdenlive:collapsed", "0"],
])]
elif name == "mask":
mask = [
self.get_element("filter", [
["mlt_service", "frei0r.saturat0r"],
["kdenlive_id", "frei0r.saturat0r"],
["Saturation", "00:00:00.000=0.001"],
]),
self.get_element("filter", [
["mlt_service", "frei0r.select0r"],
["kdenlive_id", "frei0r.select0r"],
["Color to select", "00:00:00.000=0x000000ff"],
["Invert selection", "1"],
["Selection subspace", "0"],
["Subspace shape", "0.5"],
["Edge mode", "0.9"],
["Delta R / A / Hue", "00:00:00.000=0.381"],
["Delta G / B / Chroma", "00:00:00.000=0.772"],
["Delta B / I / I", "00:00:00.000=0.522"],
["Slope", "00:00:00.000=0.515"],
["Operation", "0.5"],
])
]
return mask
elif name == "volume":
return [self.get_element("filter", [
["window", "75"],
["max_gain", "20db"],
["mlt_service", "volume"],
["kdenlive_id", "volume"],
["level", "00:00:00.000=%s" % value],
["kdenlive:collapsed", "0"],
])]
else:
return [
self.get_element("filter", [
["mlt_service", name],
["kdenlive_id", name],
] + value)
]
def properties(self, *props):
return [
self.get_element("property", attrib={"name": name}, text=str(value) if value is not None else value)
for name, value in props
]
def append_clip(self, track_id, clip):
if track_id == "V1":
track = self._v1
elif track_id == "V2":
track = self._v2
elif track_id == "A1":
track = self._a1
elif track_id == "A2":
track = self._a2
elif track_id == "A3":
track = self._a3
elif track_id == "A4":
track = self._a4
else:
print('!!', track_id)
frames = int(self._fps * clip['duration'])
self._duration[track_id] += frames
if clip.get("blank"):
track.append(
self.get_element("blank", attrib={
"length": str(frames),
})
)
return
path = clip['src']
filters = clip.get("filter", {})
#print(path, filters)
chain = self.get_chain(path)
id = get_propery(chain, "kdenlive:id")
if track_id[0] == 'A':
has_audio = False
for prop in chain.xpath('property'):
if prop.attrib['name'].endswith('stream.type') and prop.text == "audio":
has_audio = True
idx = self._tree.index(track) - 1
self._tree.insert(idx, chain)
filters_ = []
if track_id == 'V':
filters_.append({
self.get_element("filter", [
["mlt_service", "qtblend"],
["kdenlive_id", "qtblend"],
["rotate_center", "1"],
["rect", "00:00:00.000=0 0 %s %s 1.000000" % (self._width, self.height)],
["rotation", "00:00:00.000=0"],
["compositing", "0"],
["distort", "0"],
["kdenlive:collapsed", "0"],
["disable", "0"],
])
})
for ft in filters.items():
filters_ += self.get_filter(*ft)
if track_id[0] == 'A' and not has_audio:
track.append(
self.get_element("blank", attrib={
"length": str(frames),
})
)
else:
track.append(
self.get_element("entry", attrib={
"producer": chain.attrib["id"],
"in": chain.attrib["in"],
"out": str(frames - 1)
}, children=[
["kdenlive:id", id],
] + filters_),
)
chain = self.get_chain(path, id)
self._tree.append(chain)
self._main_bin.append(
self.get_element("entry", attrib={
"producer": chain.attrib["id"],
"in": chain.attrib["in"],
"out": chain.attrib["out"],
}),
)

109
sax.py Normal file
View file

@ -0,0 +1,109 @@
#!/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)

189
scripts/poster.py Executable file
View file

@ -0,0 +1,189 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from PIL import Image
from PIL import ImageDraw
import json
from optparse import OptionParser
import ox
from ox.image import drawText, wrapText
import sys
root_dir = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
static_root = os.path.join(os.path.dirname(__file__), 'data')
def render_poster(data, poster):
title = ox.decode_html(data.get('title', ''))
director = ox.decode_html(', '.join(data.get('director', [])))
if not director and "type" in data:
director = ", ".join(data["type"])
if director == "Voice Over" and data.get("batch"):
director = "%s (vo)" % (", ".join(data["batch"])).replace('Text-', '')
vo = True
else:
vo = False
year = str(data.get('year', ''))
series = data.get('isSeries', False)
oxdb_id = data['oxdbId']
imdb_id = data['id']
frame = data.get('frame')
timeline = data.get('timeline')
def get_oxdb_color(oxdb_id, series=False):
i = int(round((int(oxdb_id[2:10], 16) * 762 / pow(2, 32))))
type = data.get('type')
if type:
type = data.get('type')[0].lower()
if type == "original":
i = 0
elif type == "background":
i = 200
elif type == "foreground":
i = 400
elif type == "foreground2":
i = 500
elif type == "annimation":
i = 600
elif type == "voice over":
i = 300
if '1-' in director:
i = 300
if '2-' in director:
i = 320
if '3-' in director:
i = 340
if '4-' in director:
i = 360
if '5-' in director:
i = 380
if i < 127:
color = (127, i, 0)
elif i < 254:
color = (254 - i, 127, 0)
elif i < 381:
color = (0, 127, i - 254)
elif i < 508:
color = (0, 508 - i, 127)
elif i < 635:
color = (i - 508, 0, 127)
else:
color = (127, 0, 762 - i)
if series:
color = tuple(map(lambda x: x + 128, color))
return color
poster_width = 640
poster_height = 1024
poster_ratio = poster_width / poster_height
poster_image = Image.new('RGB', (poster_width, poster_height))
draw = ImageDraw.Draw(poster_image)
font_file = os.path.join(static_root, 'DejaVuSansCondensedBold.ttf')
font_size = {
'small': 28,
'large': 42,
}
# frame
frame_width = poster_width
frame_ratio = 4 / 3
frame_height = int(round(frame_width / frame_ratio))
if frame:
frame_image = Image.open(frame)
frame_image_ratio = frame_image.size[0] / frame_image.size[1]
if frame_ratio < frame_image_ratio:
frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.LANCZOS)
left = int((frame_image.size[0] - frame_width) / 2)
frame_image = frame_image.crop((left, 0, left + frame_width, frame_height))
else:
frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.LANCZOS)
top = int((frame_image.size[1] - frame_height) / 2)
frame_image = frame_image.crop((0, top, frame_width, top + frame_height))
poster_image.paste(frame_image, (0, 0))
# timeline
timeline_width = poster_width
timeline_height = 64
if timeline:
timeline_image = Image.open(timeline)
timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.LANCZOS)
poster_image.paste(timeline_image, (0, frame_height))
# text
text_width = poster_width
text_height = poster_height - frame_height - timeline_height
text_top = frame_height + timeline_height
text_bottom = text_top + text_height
text_margin = 16
text_color = get_oxdb_color(oxdb_id, series)
font_color = tuple(map(lambda x: x - 128 if series else x + 128, text_color))
draw.rectangle([(0, text_top), (text_width, text_bottom)], fill=text_color)
offset_top = text_top + text_margin
if vo:
vo_text = (" ".join(data["batch"])).replace("Text-", "")
drawText(poster_image, (0, 0), vo_text, font_file, 100, font_color)
if not director:
title_max_lines = 7
else:
title_max_lines = min(len(wrapText(title, text_width - 2 * text_margin, 0, font_file, font_size['large'])), 6)
director_max_lines = 9 - int((title_max_lines * 3 - 1) / 2)
if director:
lines = wrapText(director, text_width - 2 * text_margin, director_max_lines, font_file, font_size['small'])
for i, line in enumerate(lines):
size = drawText(poster_image, (text_margin, offset_top), line, font_file, font_size['small'], font_color)
offset_top += font_size['small'] + 2
offset_top += size[1] - font_size['small'] + text_margin / 2
lines = wrapText(title, text_width - 2 * text_margin, title_max_lines, font_file, font_size['large'])
for i, line in enumerate(lines):
size = drawText(poster_image, (text_margin, offset_top + 5), line, font_file, font_size['large'], font_color)
offset_top += font_size['large'] + 3
offset_top += size[1] - font_size['small'] + text_margin / 2
if year:
drawText(poster_image, (text_margin, offset_top), year, font_file, font_size['small'], font_color)
item_id = imdb_id or oxdb_id
drawText(poster_image, (text_margin, text_bottom - text_margin - font_size['large'] + 2), item_id, font_file, font_size['large'], font_color)
'''
# logo
logo_height = 32
logo_image = Image.open(os.path.join(static_root, '..', '..', 'static', 'png', 'logo.png'))
logo_width = int(round(logo_height * logo_image.size[0] / logo_image.size[1]))
logo_image = logo_image.resize((logo_width, logo_height), Image.LANCZOS)
logo_left = text_width - text_margin - logo_width
logo_top = text_bottom - text_margin - logo_height
for y in range(logo_height):
for x in range(logo_width):
poster_color = poster_image.getpixel((logo_left + x, logo_top + y))
logo_color = logo_image.getpixel((x, y))[0]
alpha = logo_image.getpixel((x, y))[3]
if series:
poster_color = tuple(map(lambda x: int(x - (logo_color - 16) * alpha / 255), poster_color))
else:
poster_color = tuple(map(lambda x: int(x + (logo_color - 16) * alpha / 255), poster_color))
poster_image.putpixel((logo_left + x, logo_top + y), poster_color)
i '''
poster_image.save(poster)
def main():
parser = OptionParser()
parser.add_option('-d', '--data', dest='data', help='json file with metadata', default=None)
parser.add_option('-p', '--poster', dest='poster', help='Poster (image file to be written)')
(options, args) = parser.parse_args()
if None in (options.data, options.poster):
parser.print_help()
sys.exit()
if options.data == '-':
data = json.load(sys.stdin)
else:
with open(options.data) as f:
data = json.load(f)
render_poster(data, options.poster)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,729 @@
'use strict';
pandora.ui.infoView = function(data, isMixed) {
isMixed = isMixed || {};
var ui = pandora.user.ui,
descriptions = [],
isMultiple = arguments.length == 2,
canEdit = pandora.hasCapability('canEditMetadata') || isMultiple || data.editable,
canRemove = pandora.hasCapability('canRemoveItems'),
css = {
marginTop: '4px',
textAlign: 'justify'
},
html,
iconRatio = ui.icons == 'posters' ? data.posterRatio : 1,
iconSize = isMultiple ? 0 : ui.infoIconSize,
iconWidth = isMultiple ? 0 : iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio),
iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio),
iconLeft = isMultiple ? 0 : iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0,
borderRadius = ui.icons == 'posters' ? 0 : iconSize / 8,
margin = 16,
nameKeys = pandora.site.itemKeys.filter(function(key) {
return key.sortType == 'person';
}).map(function(key) {
return key.id;
}),
listKeys = pandora.site.itemKeys.filter(function(key) {
return Ox.isArray(key.type);
}).map(function(key){
return key.id;
}),
specialListKeys = [].concat(
pandora.site.itemKeys.filter(function(key) {
return key.type[0] == 'date'
}).map(function(key) {
return key.id;
})
),
posterKeys = nameKeys.concat(['title', 'year']),
displayedKeys = [ // FIXME: can tis be a flag in the config?
'title', 'notes', 'name', 'summary', 'id',
'hue', 'saturation', 'lightness', 'cutsperminute', 'volume',
'user', 'rightslevel', 'bitrate', 'timesaccessed',
'numberoffiles', 'numberofannotations', 'numberofcuts', 'words', 'wordsperminute',
'duration', 'aspectratio', 'pixels', 'size', 'resolution',
'created', 'modified', 'accessed',
'random'
],
statisticsWidth = 128,
$bar = Ox.Bar({size: 16})
.bindEvent({
doubleclick: function(e) {
if ($(e.target).is('.OxBar')) {
$info.animate({scrollTop: 0}, 250);
}
}
}),
$options = Ox.MenuButton({
items: [
{
id: 'delete',
title: Ox._('Delete {0}...', [pandora.site.itemName.singular]),
disabled: !canRemove
}
],
style: 'square',
title: 'set',
tooltip: Ox._('Options'),
type: 'image',
})
.css({
float: 'left',
borderColor: 'rgba(0, 0, 0, 0)',
background: 'rgba(0, 0, 0, 0)'
})
.bindEvent({
click: function(data_) {
if (data_.id == 'delete') {
pandora.$ui.deleteItemsDialog = pandora.ui.deleteItemsDialog({
items: [data]
}).open();
}
}
})
.appendTo($bar),
$edit = Ox.MenuButton({
items: [
{
id: 'insert',
title: Ox._('Insert HTML...'),
disabled: true
}
],
style: 'square',
title: 'edit',
tooltip: Ox._('Edit'),
type: 'image',
})
.css({
float: 'right',
borderColor: 'rgba(0, 0, 0, 0)',
background: 'rgba(0, 0, 0, 0)'
})
.bindEvent({
click: function(data) {
// ...
}
})
.appendTo($bar),
$info = Ox.Element().css({overflowY: 'auto'}),
that = Ox.SplitPanel({
elements: [
{element: $bar, size: isMultiple ? 0 : 16},
{element: $info}
],
orientation: 'vertical'
});
if (!isMultiple) {
var $icon = Ox.Element({
element: '<img>',
})
.attr({
src: '/' + data.id + '/' + (
ui.icons == 'posters' ? 'poster' : 'icon'
) + '512.jpg?' + data.modified
})
.css({
position: 'absolute',
left: margin + iconLeft + 'px',
top: margin + 'px',
width: iconWidth + 'px',
height: iconHeight + 'px',
borderRadius: borderRadius + 'px',
cursor: 'pointer'
})
.bindEvent({
singleclick: toggleIconSize
})
.appendTo($info),
$reflection = $('<div>')
.addClass('OxReflection')
.css({
position: 'absolute',
left: margin + 'px',
top: margin + iconHeight + 'px',
width: iconSize + 'px',
height: iconSize / 2 + 'px',
overflow: 'hidden'
})
.appendTo($info),
$reflectionIcon = $('<img>')
.attr({
src: '/' + data.id + '/' + (
ui.icons == 'posters' ? 'poster' : 'icon'
) + '512.jpg?' + data.modified
})
.css({
position: 'absolute',
left: iconLeft + 'px',
width: iconWidth + 'px',
height: iconHeight + 'px',
borderRadius: borderRadius + 'px'
})
.appendTo($reflection),
$reflectionGradient = $('<div>')
.css({
position: 'absolute',
width: iconSize + 'px',
height: iconSize / 2 + 'px'
})
.appendTo($reflection);
}
var $text = Ox.Element()
.addClass('OxTextPage')
.css({
position: 'absolute',
left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px',
top: margin + 'px',
right: margin + statisticsWidth + margin + 'px',
})
.appendTo($info),
$statistics = $('<div>')
.css({
position: 'absolute',
width: statisticsWidth + 'px',
top: margin + 'px',
right: margin + 'px'
})
.appendTo($info);
[$options, $edit].forEach(function($element) {
$element.find('input').css({
borderWidth: 0,
borderRadius: 0,
padding: '3px'
});
});
listKeys.forEach(function(key) {
if (Ox.isString(data[key])) {
data[key] = [data[key]];
}
});
if (!canEdit) {
pandora.createLinks($info);
}
// Title -------------------------------------------------------------------
$('<div>')
.css({
marginTop: '-2px',
})
.append(
Ox.EditableContent({
editable: canEdit,
tooltip: canEdit ? pandora.getEditTooltip() : '',
placeholder: formatLight(Ox._( isMixed.title ? 'Mixed title' : 'Untitled')),
value: data.title || ''
})
.css({
marginBottom: '-3px',
fontWeight: 'bold',
fontSize: '13px'
})
.bindEvent({
submit: function(event) {
editMetadata('title', event.value);
}
})
)
.appendTo($text);
// Director, Year and Country, Language --------------------------------
renderGroup(['director', 'year']);
// Render any remaing keys defined in config
renderRemainingKeys();
renderCluster();
// Summary -----------------------------------------------------------------
if (canEdit || data.summary) {
$('<div>')
.append(
Ox.EditableContent({
clickLink: pandora.clickLink,
editable: canEdit,
format: function(value) {
return value.replace(
/<img src=/g,
'<img style="float: left; max-width: 256px; max-height: 256px; margin: 0 16px 16px 0" src='
);
},
maxHeight: Infinity,
placeholder: formatLight(Ox._( isMixed.summary ? 'Mixed Summary' : 'No Summary')),
tooltip: canEdit ? pandora.getEditTooltip() : '',
type: 'textarea',
value: data.summary || ''
})
.css(css)
.css({
marginTop: '12px',
overflow: 'hidden'
})
.bindEvent({
submit: function(event) {
editMetadata('summary', event.value);
}
})
)
.appendTo($text);
}
// Duration, Aspect Ratio --------------------------------------------------
if (!isMultiple) {
['duration', 'aspectratio'].forEach(function(key) {
var itemKey = Ox.getObjectById(pandora.site.itemKeys, key),
value = data[key] || 0;
$('<div>')
.css({marginBottom: '4px'})
.append(formatKey(itemKey.title, 'statistics'))
.append(
Ox.Theme.formatColor(null, 'gradient')
.css({textAlign: 'right'})
.html(
Ox['format' + Ox.toTitleCase(itemKey.format.type)]
.apply(null, [value].concat(itemKey.format.args))
)
)
.appendTo($statistics);
});
// Hue, Saturation, Lightness, Volume --------------------------------------
['hue', 'saturation', 'lightness', 'volume'].forEach(function(key) {
$('<div>')
.css({marginBottom: '4px'})
.append(formatKey(key, 'statistics'))
.append(
Ox.Theme.formatColor(
data[key] || 0, key == 'volume' ? 'lightness' : key
).css({textAlign: 'right'})
)
.appendTo($statistics);
});
// Cuts per Minute ---------------------------------------------------------
$('<div>')
.css({marginBottom: '4px'})
.append(formatKey('cuts per minute', 'statistics'))
.append(
Ox.Theme.formatColor(null, 'gradient')
.css({textAlign: 'right'})
.html(Ox.formatNumber(data['cutsperminute'] || 0, 3))
)
.appendTo($statistics);
}
// Rights Level ------------------------------------------------------------
var $rightsLevel = $('<div>');
$('<div>')
.css({marginBottom: '4px'})
.append(formatKey('Rights Level', 'statistics'))
.append($rightsLevel)
.appendTo($statistics);
pandora.renderRightsLevel(that, $rightsLevel, data, isMixed, isMultiple, canEdit);
// User and Groups ---------------------------------------------------------
if (!isMultiple || pandora.hasCapability('canEditUsers')) {
['user', 'groups'].forEach(function(key) {
if (key == 'groups' && isMultiple) {
return
};
var $input;
(canEdit || data[key] && data[key].length) && $('<div>')
.css({marginBottom: '4px'})
.append(formatKey(key, 'statistics'))
.append(
$('<div>')
.css({margin: '2px 0 0 -1px'}) // fixme: weird
.append(
$input = Ox.Editable({
placeholder: key == 'groups'
? formatLight(Ox._(isMixed[key] ? 'Mixed Groups' : 'No Groups'))
: isMixed[key] ? formatLight(Ox._('Mixed Users')) : '',
editable: key == 'user' && canEdit,
tooltip: canEdit ? pandora.getEditTooltip() : '',
value: isMixed[key]
? ''
: key == 'user' ? data[key] : data[key].join(', ')
})
.bindEvent(Ox.extend({
submit: function(event) {
editMetadata(key, event.value);
}
}, key == 'groups' ? {
doubleclick: canEdit ? function() {
setTimeout(function() {
if (window.getSelection) {
window.getSelection().removeAllRanges();
} else if (document.selection) {
document.selection.empty();
}
});
pandora.$ui.groupsDialog = pandora.ui.groupsDialog({
id: data.id,
name: data.title,
type: 'item'
})
.bindEvent({
groups: function(data) {
$input.options({
value: data.groups.join(', ')
});
}
})
.open();
} : function() {}
} : {}))
)
)
.appendTo($statistics);
});
}
// Created and Modified ----------------------------------------------------
if (!isMultiple && canEdit) {
['created', 'modified'].forEach(function(key) {
$('<div>')
.css({marginBottom: '4px'})
.append(formatKey(key, 'statistics'))
.append(
$('<div>').html(Ox.formatDate(data[key], '%B %e, %Y'))
)
.appendTo($statistics);
});
}
// Notes --------------------------------------------------------------------
if (canEdit) {
$('<div>')
.css({marginBottom: '4px'})
.append(
formatKey('Notes', 'statistics').options({
tooltip: Ox._('Only {0} can see and edit these comments', [
Object.keys(pandora.site.capabilities.canEditMetadata).map(function(level, i) {
return (
i == 0 ? ''
: i < Ox.len(pandora.site.capabilities.canEditMetadata) - 1 ? ', '
: ' ' + Ox._('and') + ' '
) + Ox.toTitleCase(level)
}).join('')])
})
)
.append(
Ox.EditableContent({
height: 128,
placeholder: formatLight(Ox._(isMixed.notes ? 'Mixed notes' : 'No notes')),
tooltip: pandora.getEditTooltip(),
type: 'textarea',
value: data.notes || '',
width: 128
})
.bindEvent({
submit: function(event) {
editMetadata('notes', event.value);
}
})
)
.appendTo($statistics);
}
$('<div>').css({height: '16px'}).appendTo($statistics);
function editMetadata(key, value) {
if (value != data[key]) {
var itemKey = Ox.getObjectById(pandora.site.itemKeys, key);
var edit = {id: isMultiple ? ui.listSelection : data.id};
if (key == 'title') {
edit[key] = value;
} else if (listKeys.indexOf(key) >= 0) {
edit[key] = value ? value.split(', ') : [];
} else if (specialListKeys.indexOf(key) > -1) {
edit[key] = value
? Ox.decodeHTMLEntities(value).split('; ').map(Ox.encodeHTMLEntities)
: [];
} else {
edit[key] = value ? value : null;
}
if (itemKey && itemKey.type && itemKey.type[0] == 'date') {
edit[key] = edit[key].map(pandora.cleanupDate);
}
pandora.api.edit(edit, function(result) {
if (!isMultiple) {
var src;
data[key] = result.data[key];
descriptions[key] && descriptions[key].options({
value: result.data[key + 'description']
});
Ox.Request.clearCache(); // fixme: too much? can change filter/list etc
if (result.data.id != data.id) {
pandora.UI.set({item: result.data.id});
pandora.$ui.browser.value(data.id, 'id', result.data.id);
}
pandora.updateItemContext();
pandora.$ui.browser.value(result.data.id, key, result.data[key]);
if (Ox.contains(posterKeys, key) && ui.icons == 'posters') {
src = pandora.getMediaURL('/' + data.id + '/poster512.jpg?' + Ox.uid());
$icon.attr({src: src});
$reflectionIcon.attr({src: src});
}
pandora.$ui.itemTitle
.options({
title: '<b>' + result.data.title
+ (Ox.len(result.data.director)
? ' (' + result.data.director.join(', ') + ')'
: '')
+ (result.data.year ? ' ' + result.data.year : '') + '</b>'
});
}
that.triggerEvent('change', Ox.extend({}, key, value));
});
}
}
function formatKey(key, mode) {
var item = Ox.getObjectById(pandora.site.itemKeys, key);
key = Ox._(item ? item.title : key);
mode = mode || 'text';
return mode == 'text'
? '<span style="font-weight: bold">' + Ox.toTitleCase(key) + ':</span> '
: mode == 'description'
? Ox.toTitleCase(key)
: Ox.Element()
.css({marginBottom: '4px', fontWeight: 'bold'})
.html(Ox.toTitleCase(key)
.replace(' Per ', ' per '));
}
function formatLight(str) {
return '<span class="OxLight">' + str + '</span>';
}
function formatLink(value, key, linkValue) {
linkValue = linkValue || value
linkValue = Ox.isArray(linkValue) ? linkValue: [linkValue]
return (Ox.isArray(value) ? value : [value]).map(function(value, idx) {
return key
? '<a href="/' + (
key == 'alternativeTitles' ? 'title' : key
) + '=' + pandora.escapeQueryValue(linkValue[idx]) + '">' + value + '</a>'
: value;
}).join(Ox.contains(specialListKeys, key) ? '; ' : ', ');
}
function formatValue(key, value) {
var ret;
if (nameKeys.indexOf(key) > -1) {
ret = formatLink(value.split(', '), 'name');
} else if (
listKeys.indexOf(key) > -1 && Ox.getObjectById(pandora.site.itemKeys, key).type[0] == 'date'
) {
ret = value.split('; ').map(function(date) {
date = pandora.cleanupDate(date)
return date ? formatLink(Ox.formatDate(date,
['', '%Y', '%B %Y', '%B %e, %Y'][date.split('-').length],
true
), key, date) : '';
}).join('; ');
} else if (listKeys.indexOf(key) > -1) {
ret = formatLink(value.split(', '), key);
} else if (specialListKeys.indexOf(key) > -1) {
ret = formatLink(
Ox.decodeHTMLEntities(value).split('; ').map(Ox.encodeHTMLEntities),
key
);
} else if (['year', 'country'].indexOf(key) > -1) {
ret = formatLink(value, key);
} else {
ret = value;
}
return ret;
}
function getValue(key, value) {
return !value ? ''
: Ox.contains(specialListKeys, key) ? value.join('; ')
: Ox.contains(listKeys, key) ? value.join(', ')
: value;
}
function renderGroup(keys) {
var $element;
keys.forEach(function(key) { displayedKeys.push(key) });
if (canEdit || keys.filter(function(key) {
return data[key];
}).length) {
$element = $('<div>').addClass('OxSelectable').css(css);
keys.forEach(function(key, i) {
if (canEdit || data[key]) {
if ($element.children().length) {
$('<span>').html('; ').appendTo($element);
}
$('<span>').html(formatKey(key)).appendTo($element);
Ox.EditableContent({
clickLink: pandora.clickLink,
editable: canEdit,
format: function(value) {
return formatValue(key, value);
},
placeholder: formatLight(Ox._(isMixed[key] ? 'mixed' : 'unknown')),
tooltip: canEdit ? pandora.getEditTooltip() : '',
value: getValue(key, data[key])
})
.bindEvent({
submit: function(data) {
editMetadata(key, data.value);
}
})
.appendTo($element);
if (isMixed[key] && Ox.contains(listKeys, key)) {
pandora.ui.addRemoveKeyDialog({
ids: ui.listSelection,
key: key,
section: ui.section
}).appendTo($element)
}
}
});
$element.appendTo($text);
}
return $element;
}
function renderRemainingKeys() {
var keys = pandora.site.itemKeys.filter(function(item) {
return item.id != '*' && item.type != 'layer' && !Ox.contains(displayedKeys, item.id);
}).map(function(item) {
return item.id;
});
if (keys.length) {
renderGroup(keys)
}
}
function renderCluster() {
if (isMultiple) {
return
}
var $element = $('<div>').addClass('OxSelectable').html("<b>Related Videos:</b>");
var title = data.title.replace('_bg', '').replace('_fg', '')
var request = {
query: {
conditions: [
{key: 'title', value: title}
],
operator: '&'
},
keys: ['title', 'type', 'id', 'batch'],
range: [0, 10]
};
$element.appendTo($text);
pandora.api.find(request, function(response) {
response.data.items.forEach(item => {
if (item.id != data.id) {
var type = item.type ? item.type[0] : 'Unknown'
if (type == 'Voice Over' && item.batch) {
type = item.batch
}
$element.append(
` <a href="/${item.id}/info">${type}</a>`
)
}
})
$element.append(`[<a href="/grid/title/title=${pandora.escapeQueryValue(title)}">all</a>]`)
pandora.createLinks($element)
})
}
function toggleIconSize() {
iconSize = iconSize == 256 ? 512 : 256;
iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio);
iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio);
iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0,
borderRadius = ui.icons == 'posters' ? 0 : iconSize / 8;
$icon.animate({
left: margin + iconLeft + 'px',
width: iconWidth + 'px',
height: iconHeight + 'px',
borderRadius: borderRadius + 'px'
}, 250);
$reflection.animate({
top: margin + iconHeight + 'px',
width: iconSize + 'px',
height: iconSize / 2 + 'px'
}, 250);
$reflectionIcon.animate({
left: iconLeft + 'px',
width: iconWidth + 'px',
height: iconHeight + 'px',
borderRadius: borderRadius + 'px'
}, 250);
$reflectionGradient.animate({
width: iconSize + 'px',
height: iconSize / 2 + 'px'
}, 250);
$text.animate({
left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px',
}, 250);
pandora.UI.set({infoIconSize: iconSize});
}
that.resizeElement = function() {
// overwrite splitpanel resize
};
that.reload = function() {
var src = src = '/' + data.id + '/' + (
ui.icons == 'posters' ? 'poster' : 'icon'
) + '512.jpg?' + Ox.uid();
$icon.attr({src: src});
$reflectionIcon.attr({src: src});
iconSize = iconSize == 256 ? 512 : 256;
iconRatio = ui.icons == 'posters'
? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1;
toggleIconSize();
};
that.bindEvent({
mousedown: function() {
setTimeout(function() {
!Ox.Focus.focusedElementIsInput() && that.gainFocus();
});
},
pandora_icons: that.reload,
pandora_showsiteposters: function() {
ui.icons == 'posters' && that.reload();
}
});
return that;
};

BIN
title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

32
utils.py Normal file
View file

@ -0,0 +1,32 @@
def upgrade_originals():
import item.models
import itemlist.models
nt = itemlist.models.List.objects.get(name='No Type')
no = itemlist.models.List.objects.get(name='New Originals')
for i in nt.get_items(nt.user):
orig = item.models.Item.objects.get(data__title=i.get('title'), data__type=["Original"])
print(i, orig)
orig.files.all().update(selected=False)
i.files.all().update(item=orig)
orig.save()
orig.remove_poster()
orig.make_poster()
i.data['type'] = ['Empty']
i.save()
no.items.add(orig)
def remove_deselected_files():
import itemlist.models
il = itemlist.models.List.objects.get(name='New Originals')
for i in il.items.all():
changed = False
for f in i.files.filter(selected=False):
f.data.delete()
f.delete()
changed = True
if changed:
i.save()