diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e5938ae --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/management/commands/render.py b/management/commands/render.py new file mode 100644 index 0000000..5ae57a1 --- /dev/null +++ b/management/commands/render.py @@ -0,0 +1,28 @@ +import json +import os + +from django.core.management.base import BaseCommand +from django.conf import settings + +from ...render import compose, render + + +class Command(BaseCommand): + help = 'generate symlinks to clips and clips.json' + + 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="150", help='target duration in seconds') + parser.add_argument('--offset', action='store', dest='offset', default="1024", help='inital offset in pi') + + def handle(self, **options): + prefix = options['prefix'] + target = int(options['duration']) + base = int(options['offset']) + + with open(os.path.join(prefix, "clips.json")) as fd: + clips = json.load(fd) + scene = compose(clips, target=target, base=base) + render(prefix, scene) + with open(os.path.join(prefix, 'scene-%s.json' % base), 'w') as fd: + json.dump(scene, fd, indent=2, ensure_ascii=False) diff --git a/pi.py b/pi.py new file mode 100644 index 0000000..491a633 --- /dev/null +++ b/pi.py @@ -0,0 +1,20 @@ +from mpmath import mp +mp.dps = 10000 +PI = str(mp.pi).replace('.', '') + +class random(object): + PI = str(mp.pi).replace('.', '') + + def __init__(self, offset=0): + self.position = offset + 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) + diff --git a/render.py b/render.py new file mode 100644 index 0000000..65cd41a --- /dev/null +++ b/render.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 +import json +import os +import subprocess +import sys +import time + +import ox +from pi import random +from render_kdenlive import KDEnliveProject + + +def random_choice(seq, items, pop=False): + 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) + if pop: + return items.pop(r) + return items[r] + +def chance(seq, chance): + return (seq() / 10) >= chance + + +def compose(clips, target=150, base=1024): + length = 0 + scene = { + 'front': { + 'V1': [], + 'V2': [], + }, + 'back': { + 'V1': [], + 'V2': [], + }, + 'audio': { + 'A1': [], + 'A2': [], + 'A3': [], + 'A4': [], + } + } + seq = random(base) + while target - length > 10 and clips: + clip = random_choice(seq, clips, True) + if length + clip['duration'] > target: + break + length += clip['duration'] + + scene['front']['V1'].append({ + 'duration': clip['duration'], + 'src': clip['foreground'], + "filter": { + 'transparency': seq() / 10, + } + }) + + transparency = seq() / 10 + # coin flip which site is visible (50% chance) + if chance(seq, 0.5): + transparency_front = transparency + transparency_back = 0 + else: + transparency_back = transparency + transparency_front = 0 + scene['front']['V2'].append({ + 'duration': clip['duration'], + 'src': clip['background'], + "filter": { + 'transparency': transparency_front + } + }) + scene['back']['V1'].append({ + 'duration': clip['duration'], + 'src': clip['background'], + "filter": { + 'transparency': transparency_back + } + }) + scene['back']['V2'].append({ + 'duration': clip['duration'], + 'src': clip['original'], + "filter": { + 'transparency': seq() / 10, + } + }) + # 50 % chance to blur original from 0 to 30 + if chance(seq, 0.5): + blur = seq() * 3 + scene['back']['V2'][-1]['filter']['blur'] = blur + scene['audio']['A1'].append({ + 'duration': clip['duration'], + 'src': clip['original'], + }) + scene['audio']['A2'].append({ + 'duration': clip['duration'], + 'src': clip['foreground'], + }) + return scene + + +def render(root, scene): + fps = 24 + for timeline, data in scene.items(): + print(timeline) + project = KDEnliveProject(root) + + tracks = [] + for track, clips in data.items(): + print(track) + for clip in clips: + project.append_clip(track, clip) + + with open(os.path.join(prefix, "%s.kdenlive" % timeline), 'w') as fd: + fd.write(project.to_xml()) + diff --git a/render_kdenlive.py b/render_kdenlive.py new file mode 100644 index 0000000..6daf309 --- /dev/null +++ b/render_kdenlive.py @@ -0,0 +1,501 @@ +#!/usr/bin/python3 +from collections import defaultdict +import subprocess +import lxml.etree +import uuid +import os + +_IDS = defaultdict(int) + +def get_propery(element, name): + return element.xpath('property[@name="%s"]' % name)[0].text + + + +class KDEnliveProject: + + def to_xml(self): + track = self._main_tractor.xpath(".//track")[0] + duration = max(self._duration.values()) + track.attrib["in"] = self._sequence.attrib["in"] = self._main_tractor.attrib["in"] = "0" + track.attrib["out"] = self._sequence.attrib["out"] = self._main_tractor.attrib["out"] = str(duration - 1) + 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 "\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"], + ]), + a2 := self.get_element("playlist", children=[ + ["kdenlive:audio_track", "1"], + ]), + a2e := self.get_element("playlist", children=[ + ["kdenlive:audio_track", "1"], + ]), + t0 := 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"], + ]), + t1 := 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"], + ]), + ]), + v2 := self.get_element("playlist", children=[ + ]), + v2e := 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": v2.attrib["id"]}), + self.get_element("track", attrib={"hide": "audio", "producer": v2e.attrib["id"]}), + ]), + v1 := self.get_element("playlist", children=[ + ]), + v1e := 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": v1.attrib["id"]}), + self.get_element("track", attrib={"hide": "audio", "producer": v1e.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": t0.attrib["id"]}), + self.get_element("track", attrib={"producer": t1.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"], + ["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", "4"], + ["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._v1 = v1 + self._v2 = v2 + self._a1 = a1 + self._a2 = a2 + + 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 = subprocess.check_output(['melt', file, '-consumer', 'xml']) + 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", "1"), + ("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_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"], + ])] + if 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"], + ])] + if 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 + 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): + path = clip['src'] + filters = clip.get("filter", {}) + frames = int(self._fps * clip['duration']) + self._duration[track_id] += frames + print(path, filters) + chain = self.get_chain(path) + id = get_propery(chain, "kdenlive:id") + 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 + else: + print('!!', track_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), + }, 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"], + }), + )