pandora_cdosea/render.py

472 lines
15 KiB
Python
Executable File

#!/usr/bin/python3
from argparse import ArgumentParser
from collections import defaultdict
from glob import glob
import json
import os
import string
import subprocess
import sys
from pi import random
from keywords import KEYWORDS
import ox
import ox.web.auth
base_url = 'http://127.0.0.1:2620'
FRAME_DURATION = 1/60
MAX_DURATION = 40
HIDDEN_TAGS = [
"women with white males",
"gene z hanrahan"
]
# items to not use at all
BLACKLIST = [
'XN'
]
api = None
def get_api():
global api
if not api:
api = ox.API(base_url + '/api/')
api.signin(**ox.web.auth.get('cdosea'))
if os.path.exists('PATHS.json'):
PATHS = json.load(open('PATHS.json'))
else:
PATHS = {}
if os.path.exists('CLIPS.json'):
CLIPS = json.load(open('CLIPS.json'))
else:
CLIPS = {}
if not os.path.exists('MUSIC.json'):
MUSIC = defaultdict(list)
for letter in os.listdir('music'):
for d in range(10):
path = os.path.join('music', letter, '%d.mp3' % d)
MUSIC[letter].append({
'path': path,
'duration': ox.avinfo(path)['duration']
})
with open('MUSIC.json', 'w') as fd:
json.dump(MUSIC, fd, indent=2, sort_keys=True)
else:
MUSIC = json.load(open('MUSIC.json'))
if not os.path.exists('VOCALS.json'):
VOCALS = defaultdict(list)
for letter in os.listdir('vocals'):
for fn in sorted(os.listdir(os.path.join('vocals', letter))):
path = os.path.join('vocals', letter, fn)
VOCALS[letter].append({
'path': path,
'duration': ox.avinfo(path)['duration']
})
while len(VOCALS[letter]) < 10:
VOCALS[letter] += VOCALS[letter]
VOCALS[letter] = VOCALS[letter][:10]
with open('VOCALS.json', 'w') as fd:
json.dump(VOCALS, fd, indent=2, sort_keys=True)
else:
VOCALS = json.load(open('VOCALS.json'))
if not os.path.exists('DRONES.json'):
DRONES = defaultdict(list)
prefix = 'drones_wav'
for letter in os.listdir(prefix):
for fn in sorted(os.listdir(os.path.join(prefix, letter))):
path = os.path.join(prefix, letter, fn)
DRONES[letter[0]].append({
'path': path,
'duration': ox.avinfo(path)['duration']
})
with open('DRONES.json', 'w') as fd:
json.dump(DRONES, fd, indent=2, sort_keys=True)
else:
DRONES = json.load(open('DRONES.json'))
def get_path(id):
global PATHS
if id not in PATHS:
get_api()
info = api.findMedia({
'query': {
'conditions': [
{'key': 'id', 'operator': '==', 'value': id}
]
},
'keys': ['id', 'extension'],
'range': [0, 1]
})['data']['items'][0]
path = os.path.join('cache', '%s.%s' % (info['id'], info['extension']))
h = info['id']
source = '/srv/pandora/data/media/%s/%s/%s/%s/data.*' % (
h[:2], h[2:4], h[4:6], h[6:]
)
source = glob(source)[0]
if not os.path.exists(path):
if not os.path.exists(source):
print('WTF', source)
sys.exit(1)
os.symlink(source, path)
'''
url = '%s/%s/download/source/' % (base_url, id)
print('get video', url)
'''
PATHS[id] = path
with open('PATHS.json', 'w') as fd:
json.dump(PATHS, fd, indent=4, sort_keys=True)
return PATHS[id]
def get_clips(tag):
global CLIPS
if tag not in CLIPS:
get_api()
clips = api.findAnnotations({
'query': {
'conditions': [
{'key': 'layer', 'operator': '==', 'value': 'keywords'},
{'key': 'value', 'operator': '==', 'value': tag}
],
'operator': '&'
},
'keys': ['id', 'in', 'out'],
'range': [0, 90000]})['data']['items']
clips = [clip for clip in clips if clip['id'].split('/')[0] not in BLACKLIST]
for clip in clips:
clip['path'] = get_path(clip['id'].split('/')[0])
# or use round?
clip['in'] = int(clip['in'] / FRAME_DURATION) * FRAME_DURATION
clip['out'] = int(clip['out'] / FRAME_DURATION) * FRAME_DURATION
clip['duration'] = clip['out'] - clip['in']
clip['tag'] = tag
clips = [clip for clip in clips if clip['duration'] and clip['duration'] <= MAX_DURATION]
for clip in clips:
fduration = ox.avinfo(clip['path'])['duration']
if clip['out'] > fduration:
if clip['in'] == 0:
clip['out'] = fduration
clip['duration'] = clip['out'] - clip['in']
else:
print('FAIL', clip, fduration)
sys.exit(1)
CLIPS[tag] = list(sorted(clips, key=lambda c: c['id']))
with open('CLIPS.json', 'w') as fd:
json.dump(CLIPS, fd, indent=4, sort_keys=True)
return CLIPS[tag].copy()
def random_choice(seq, items):
n = n_ = len(items) - 1
#print('len', n)
if n == 0:
return items[0]
r = seq()
base = 10
while n > 10:
n /= 10
#print(r)
r += seq()
base += 10
r = int(n_ * r / base)
#print('result', r, items)
return items[r]
def splitint(number, by):
div = int(number/by)
mod = number % by
return [div + 1 if i > (by - 1 - mod) else div for i in range(by)]
def filter_clips(clips, duration, max_duration=0):
# 1 minute
blur = 0.5
low = 1
high = 10
# 2 minute
blur = 1
low = 2
high = 20
buckets = {}
clips_ = []
for tag in clips:
for clip in clips[tag]:
clip['tag'] = tag
clips_.append(clip)
clips_.sort(key=lambda c: c['duration'])
#print(clips_)
size = splitint(len(clips_), 10)
p = 0
for i in range(10):
buckets[i+1] = clips_[p:+p+size[i]]
p += size[i]
clips_ = {}
#print(buckets[duration])
for clip in buckets[duration]:
if clip['tag'] not in clips_:
clips_[clip['tag']] = []
clips_[clip['tag']].append(clip)
return clips_
def add_blank(track, d):
if track and track[-1].get('blank'):
track[-1]['duration'] += d
else:
blank = {'blank': True, 'duration': d}
track.append(blank)
return d
def sequence(seq, letter):
tags = KEYWORDS[letter]
clips = {tag: get_clips(tag) for tag in tags}
all_clips = clips.copy()
result = {
'clips': [],
'text': [],
'vocals': [],
'music': [],
'drones0': [],
'drones1': [],
}
duration = 0
MAX_DURATION = 60 * 2 + 5
MIN_DURATION = 60 * 2 - 4
# add 1 black frame for sync playback
duration = 1 * FRAME_DURATION
result['clips'].append({'black': True, 'duration': duration})
while duration < MAX_DURATION and not duration >= MIN_DURATION:
# clip duration: 1-10
n = seq()
if n == 0:
n = 10
max_duration = MAX_DURATION - duration
clips_n = filter_clips(clips, n, max_duration)
tags_n = [tag for tag in tags if tag in clips_n]
if not tags_n:
print('MISSING CLIPS, fall back to all clips!', letter, n)
clips_n = filter_clips(all_clips, n, max_duration)
tags_n = [tag for tag in tags if tag in clips_n]
if not tags_n:
print('NO tags for', letter, n)
sys.exit(1)
tag = random_choice(seq, tags_n)
#if 'tiger' in tags_n:
# tag = 'tiger'
clip = random_choice(seq, clips_n[tag])
duration += clip['duration']
result['clips'].append(clip.copy())
# no reuse
for t in clips:
clips[t] = [c for c in clips[t] if not c['id'] == clip['id']]
'''
for clip in result['clips']:
if seq() == 0:
clip['black'] = True
'''
# text overlay
position = last_text = 0
tags_text = []
# no overlay for the first 2 frames
position = last_text = add_blank(result['text'], 2 * FRAME_DURATION)
while position < duration:
n = seq()
if n == 0:
blank = {'blank': True, 'duration': position - last_text}
if blank['duration']:
result['text'].append(blank)
n = seq()
if n == 0:
n = 10
n = min(n, duration-position)
if not tags_text:
tags_text = list(sorted(set(tags)))
tags_text = [t for t in tags_text if t not in HIDDEN_TAGS]
ttag = random_choice(seq, tags_text)
tags_text.remove(ttag)
text = {
'text': ttag,
'duration': n
}
result['text'].append(text)
position += n
last_text = position
else:
position += n
if last_text < duration:
blank = {'blank': True, 'duration': duration - last_text}
result['text'].append(blank)
# music
track = 'music'
if letter in MUSIC:
position = 0
while position < duration:
n = seq()
if n < 5:
n = seq()
position += add_blank(result[track], min(n, duration-position))
else:
n = seq()
clip = MUSIC[letter][n]
position += clip['duration']
if result[track] and position > duration \
and result[track][-1].get('blank') \
and result[track][-1]['duration'] > clip['duration']:
result[track][-1]['duration'] -= (position-duration)
position = duration
if not result[track][-1]['duration']:
result[track].pop(-1)
if position <= duration:
result[track].append(clip)
else:
position -= clip['duration']
break
if position < duration:
position += add_blank(result[track], duration - position)
# vocals
track = 'vocals'
if letter in VOCALS:
position = 0
loop = 0
used = []
while position < duration:
n = seq()
# vocals should start after one blank
if len(result[track]) and result[track][-1].get('blank'):
n = 10
# 50 % chance of a silence of up to 5 seconds
if n < 5:
n = seq() / 2 # (0 - 5 seconds)
position += add_blank(result[track], min(n, duration-position))
else:
clip = None
if len(used) == len(VOCALS[letter]):
break
while clip is None or clip['path'] in used:
n = seq()
clip = VOCALS[letter][n]
position += clip['duration']
if result[track] and position > duration \
and result[track][-1].get('blank') \
and result[track][-1]['duration'] > clip['duration']:
result[track][-1]['duration'] -= (position-duration)
position = duration
if not result[track][-1]['duration']:
result[track].pop(-1)
if position <= duration:
used.append(clip['path'])
result[track].append(clip)
else:
position -= clip['duration']
if duration - position < 10 or loop > 10:
break
loop += 1
if position < duration:
position += add_blank(result[track], duration - position)
'''
if letter in VOCALS:
n = seq()
clip = VOCALS[letter][n]
n = 1.0 / (seq() + 1) # 0.1 - 1
silence = duration - clip['duration']
silence_start = n * silence
blank = {'blank': True, 'duration': silence_start}
if n != 0:
result['vocals'].append(blank)
result['vocals'].append(clip)
if n != 1:
blank = {'blank': True, 'duration': silence - silence_start}
result['vocals'].append(blank)
'''
# drones
if letter in DRONES:
for track in ('drones0', 'drones1'):
position = 0
while position < duration:
n = seq()
if n == 9:
position += add_blank(result[track], min(3.141, duration - position))
else:
clip = DRONES[letter][n]
if position + clip['duration'] < duration:
position += clip['duration']
result[track].append(clip)
else:
c = clip.copy()
c['duration'] = duration - position
result[track].append(c)
position += c['duration']
if position < duration:
position += add_blank(result[track], duration - position)
for track in result:
if result[track]:
tduration = sum([c['duration'] for c in result[track]])
if not abs(tduration - duration) < 0.000001:
raise Exception('invalid duration on track: %s %s vs %s %s' % (track, tduration, duration, result[track]))
result['gongs'] = {
'tracks': 100 + seq() * 23,
'offset': seq.position,
}
return result
if __name__ == '__main__':
usage = "usage: %(prog)s [options] xml"
parser = ArgumentParser(usage=usage)
parser.add_argument('-p', '--prefix', dest='prefix',
help='version prefix', default='.')
parser.add_argument('files', metavar='path', type=str, nargs='*', help='json files')
opts = parser.parse_args()
render_xml = opts.files != ['json']
for n in range(10):
seq = random(n * 1000)
#for letter in ('T', 'W'):
for letter in string.ascii_uppercase:
r = sequence(seq, letter)
tjson = os.path.join(opts.prefix, 'output/%02d/%s.json' % (n, letter))
folder = os.path.dirname(tjson)
if not os.path.exists(folder):
ox.makedirs(folder)
if os.path.exists(tjson):
with open(tjson, 'r') as fd:
old = fd.read()
else:
old = None
current = json.dumps(r, indent=4, sort_keys=True)
#print(current)
#print(sum([c['duration'] for c in r['clips']]))
if current != old:
with open(tjson, 'w') as fd:
fd.write(current)
if render_xml:
txml = tjson.replace('.json', '.xml')
if not os.path.exists(txml) or \
os.path.getmtime(tjson) < os.path.getmtime('render_mlt.py') or \
os.path.getmtime(txml) < os.path.getmtime(tjson):
subprocess.call(['./render_mlt.py', '--prefix', opts.prefix, tjson])
#subprocess.call(['./render_audio.py', tjson])