#!/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])