git @ Cat's Eye Technologies kinoje / 0.8
Merge pull request #7 from catseye/develop-0.8 Develop 0.8 Chris Pressey authored 1 year, 10 months ago GitHub committed 1 year, 10 months ago
10 changed file(s) with 127 addition(s) and 67 deletion(s). Raw diff Collapse all Expand all
00 kinoje
11 ======
22
3 _Version 0.7_
3 _Version 0.8_
44 | _Entry_ [@ catseye.tc](https://catseye.tc/node/kinoje)
55 | _See also:_ [Canvas Feedback](https://github.com/catseye/Canvas-Feedback#readme)
66
1717
1818 The following are required:
1919
20 * **Python** 2.7 or 3.x — to run the script
20 * **Python** 3.x — to run the script (2.7 may or may not work)
2121 * **PyYAML** and **Jinja2** — to fill out the templates
2222 * something to create images from filled-out templates — typically **POV-Ray** or **rsvg**
2323 * **ffmpeg** or **ImageMagick** — to compile the images into a movie file
2424
2525 You might also find VLC useful, for viewing the final movie file.
2626
27 On Ubuntu 16.04, you can install these with:
27 On Ubuntu 20.04, you can install these with:
2828
29 pip install --user Jinja2 PyYAML
29 pip install --user Jinja2 PyYAML # you may want to make a virtualenv first
3030 sudo apt install povray povray-includes librsvg2 ffmpeg imagemagick vlc
3131
3232 (Or, if you would like to use Docker, you can pull a Docker image from
3737
3838 bin/kinoje eg/moebius.yaml
3939
40 Since no output filename was given, kinoje assumes MP4 format and automatically picks a reasonable
41 filename, in this case `moebius.mp4`.
40 Since no output filename was given, kinoje assumes MPEG Layer 4 format and automatically
41 picks a reasonable filename, in this case `moebius.mp4`.
42
43 Other Invokation Options
44 ------------------------
4245
4346 You can also ask it to create a GIF by specifying an output filename with that as its file extension:
4447
4548 bin/kinoje eg/squares.yaml -o squares.gif
49
50 Multiple configuration files can be specified on the command line; successive
51 configurations will act as overlays, overriding the settings in them. In fact
52 individual settings can be given in the format `+name=value`. For example,
53
54 bin/kinoje eg/moebius.yaml overlays/tiny.yaml +duration=4.0
55
56 The `--work-dir` option can be given to make `kinoje` store its working
57 files in a specified directory. The directory will be created if it does
58 not yet exist. If the directory contains incomplete working files from a
59 previous run of `kinoje`, the process will continue where it left off.
4660
4761 Theory of Operation
4862 -------------------
0 width: 1280
1 height: 800
2
0 width: 640
1 height: 400
2
0 width: 320
1 height: 200
2
0 width: 160
1 height: 100
2
11 import os
22 import sys
33
4 from kinoje.utils import BaseProcessor, Executor, load_config_file, zrange
4 from kinoje.utils import BaseProcessor, load_config_file, zrange
55
66
77 SUPPORTED_OUTPUT_FORMATS = ('.m4v', '.mp4', '.gif')
4949
5050 # -strip is there to force convert to process all input files. (if no transformation is given,
5151 # it can sometimes stop reading input files. leading to skippy animations. who knows why.)
52 self.exe.do_it("convert -delay %s -loop 0 %s -strip %s" % (
53 delay, filespec, self.outfilename
54 ))
52 self.exe.do_it(
53 ["convert", "-delay", delay, "-loop", "0", filespec, "-strip", self.outfilename],
54 shell=False
55 )
5556
5657 def view(self):
57 self.exe.do_it("eog %s" % self.outfilename)
58 self.exe.do_it(["eog", self.outfilename], shell=False)
5859
5960
6061 class MpegCompiler(Compiler):
6263 def compile(self, num_frames):
6364 ifmt = os.path.join(self.dirname, self.frame_fmt)
6465 # fun fact: even if you say -r 30, ffmpeg still picks 25 fps
65 cmd = "ffmpeg -i %s -c:v libx264 -profile:v baseline -pix_fmt yuv420p -r %s -y %s" % (
66 ifmt, int(self.config['fps']), self.outfilename
66 self.exe.do_it(
67 ["ffmpeg", "-i", ifmt, "-c:v", "libx264", "-profile:v", "baseline", "-pix_fmt", "yuv420p",
68 "-r", str(int(self.config['fps'])), "-y", self.outfilename],
69 shell=False
6770 )
68 self.exe.do_it(cmd)
6971
7072 def view(self):
71 self.exe.do_it("vlc %s" % self.outfilename)
73 self.exe.do_it(["vlc", self.outfilename], shell=False)
7274
7375
7476 def main():
9496 argparser.add_argument("--view", default=False, action='store_true',
9597 help="Display the resultant movie."
9698 )
97 argparser.add_argument('--version', action='version', version="%(prog)s 0.7")
99 argparser.add_argument('--version', action='version', version="%(prog)s 0.8")
98100
99101 options = argparser.parse_args(sys.argv[1:])
100102
55
66 from jinja2 import Template
77
8 from kinoje.utils import BaseProcessor, Executor, fmod, tween, load_config_file, items, zrange
8 from kinoje.utils import BaseProcessor, fmod, tween, load_config_file, items, zrange
99
1010
1111 class Expander(BaseProcessor):
2020 self.fun_context[key] = eval("lambda x: " + value)
2121
2222 def fillout_template(self, frame, t):
23 output_filename = os.path.join(self.dirname, "%08d.txt" % frame)
24 if os.path.isfile(output_filename):
25 return
2326 context = copy(self.config)
2427 context.update(self.fun_context)
2528 context.update({
3033 'tween': tween,
3134 'fmod': fmod,
3235 })
33 output_filename = os.path.join(self.dirname, "%08d.txt" % frame)
3436 with open(output_filename, 'w') as f:
3537 f.write(self.template.render(context))
3638
5153 argparser.add_argument('instantsdir', metavar='DIRNAME', type=str,
5254 help='Directory that will be populated with instants (text files describing frames)'
5355 )
54 argparser.add_argument('--version', action='version', version="%(prog)s 0.7")
56 argparser.add_argument('--version', action='version', version="%(prog)s 0.8")
5557
5658 options = argparser.parse_args(sys.argv[1:])
5759
0 from datetime import datetime, timedelta
10 from argparse import ArgumentParser
21 import os
3 import re
42 import sys
5 from tempfile import mkdtemp, mkstemp
3 from tempfile import mkdtemp
64
75 try:
86 from tqdm import tqdm
1311 from kinoje.renderer import Renderer
1412 from kinoje.compiler import Compiler, SUPPORTED_OUTPUT_FORMATS
1513
16 from kinoje.utils import LoggingExecutor, load_config_file
14 from kinoje.utils import LoggingExecutor, load_config_files
1715
1816
1917 def main():
2018 argparser = ArgumentParser()
2119
22 argparser.add_argument('configfile', metavar='FILENAME', type=str,
20 argparser.add_argument('configfiles', metavar='FILENAME', type=str, nargs='+',
2321 help='A YAML file containing the template to render for each frame, '
24 'as well as configuration for rendering the template.'
22 'as well as configuration for rendering the template. '
23 'If multiple such configuration files are specified, successive '
24 'files are applied as overlays.'
2525 )
2626 argparser.add_argument('-o', '--output', metavar='FILENAME', type=str, default=None,
2727 help='The movie file to create. The extension of this filename '
2929 'If not given, a default name will be chosen based on the '
3030 'configuration filename with a .mp4 extension added.' % (SUPPORTED_OUTPUT_FORMATS,)
3131 )
32 argparser.add_argument('--version', action='version', version="%(prog)s 0.7")
32 argparser.add_argument('-d', '--work-dir', metavar='DIRNAME', type=str, default=None,
33 help='The directory to store intermediate files in while creating '
34 'this movie. If not given, a directory will be created in '
35 'the system temporary directory.'
36 )
37 argparser.add_argument('--version', action='version', version="%(prog)s 0.8")
3338
34 options, unknown = argparser.parse_known_args(sys.argv[1:])
35 remainder = ' '.join(unknown)
39 options, _unknown = argparser.parse_known_args(sys.argv[1:])
3640
3741 if options.output is None:
38 (configbase, configext) = os.path.splitext(os.path.basename(options.configfile))
42 (configbase, configext) = os.path.splitext(os.path.basename(options.configfiles[0]))
3943 output_filename = configbase + '.mp4'
4044 else:
4145 output_filename = options.output
4246
4347 CompilerClass = Compiler.get_class_for(output_filename)
4448
45 config = load_config_file(options.configfile)
49 config = load_config_files(options.configfiles)
4650
47 fd, log_filename = mkstemp()
51 if options.work_dir:
52 work_dir = options.work_dir
53 if not os.path.isdir(work_dir):
54 os.mkdir(work_dir)
55 else:
56 work_dir = mkdtemp()
57
58 # TODO: append to this log if it already exists
59 log_filename = os.path.join(work_dir, 'kinoje.log')
4860 exe = LoggingExecutor(log_filename)
4961
50 instants_dir = mkdtemp()
51 frames_dir = mkdtemp()
62 instants_dir = os.path.join(work_dir, 'instants')
63 if not os.path.isdir(instants_dir):
64 os.mkdir(instants_dir)
5265
53 print('expanding template to instants...')
66 frames_dir = os.path.join(work_dir, 'frames')
67 if not os.path.isdir(frames_dir):
68 os.mkdir(frames_dir)
69
70 print('expanding template to instants in {}...'.format(instants_dir))
5471 expander = Expander(config, instants_dir, exe=exe, tqdm=tqdm)
5572 expander.expand_all()
5673
57 print('rendering instants to frames...')
74 print('rendering instants to frames in {}...'.format(frames_dir))
5875 renderer = Renderer(config, instants_dir, frames_dir, exe=exe, tqdm=tqdm)
5976 renderer.render_all()
6077
61 print('compiling frames to movie...')
78 print('compiling frames to movie file "{}"...'.format(output_filename))
6279 compiler = CompilerClass(config, frames_dir, output_filename, exe=exe, tqdm=tqdm)
6380 compiler.compile_all()
6481
6582 exe.close()
66 os.close(fd)
22 import os
33 import sys
44
5 import yaml
6 try:
7 from yaml import CLoader as Loader
8 except ImportError:
9 from yaml import Loader
10
11 from kinoje.utils import BaseProcessor, Executor, load_config_file
5 from kinoje.utils import BaseProcessor, load_config_file
126
137
148 class Renderer(BaseProcessor):
3024 frame = int(match.group(1))
3125 destname = "%08d.png" % frame
3226 full_destname = os.path.join(self.dest, destname)
27 if os.path.isfile(full_destname):
28 continue
3329 self.render(frame, full_srcname, full_destname)
3430
3531 def render(self, frame, full_srcname, full_destname):
5551 argparser.add_argument('framesdir', metavar='DIRNAME', type=str,
5652 help='Directory that will be populated with images, one for each frame'
5753 )
58 argparser.add_argument('--version', action='version', version="%(prog)s 0.7")
54 argparser.add_argument('--version', action='version', version="%(prog)s 0.8")
5955
6056 options = argparser.parse_args(sys.argv[1:])
6157
00 import os
1 import re
12 import sys
23 from subprocess import check_call
34
2627
2728
2829 def load_config_file(filename):
30 return load_config_files([filename])
31
32
33 def load_config_files(filenames):
2934 import yaml
3035 try:
3136 from yaml import CLoader as Loader
3237 except ImportError:
3338 from yaml import Loader
3439
35 with open(filename, 'r') as file_:
36 config = yaml.load(file_, Loader=Loader)
40 config = {}
41 for filename in filenames:
42 match = re.match(r'^\+(.*?)\=(.*?)$', filename)
43 if match:
44 config[match.group(1)] = float(match.group(2))
45 else:
46 with open(filename, 'r') as file_:
47 config.update(yaml.load(file_, Loader=Loader))
3748
3849 config['libdir'] = os.path.dirname(filename) or '.'
3950
5768 return config
5869
5970
60 class LoggingExecutor(object):
71 class Executor(object):
72 def __init__(self):
73 self.log = sys.stdout
74
75 def do_it(self, cmd, shell=True, **kwargs):
76 disp_cmd = cmd if shell else ' '.join(cmd)
77 self.log.write('>>> {}\n'.format(disp_cmd))
78 self.log.flush()
79 try:
80 check_call(cmd, shell=shell, stdout=self.log, stderr=self.log, **kwargs)
81 except Exception as exc:
82 self.close()
83 self.cleanup(exc)
84
85 def cleanup(self, exc):
86 print(str(exc))
87 sys.exit(1)
88
89 def close(self):
90 pass
91
92
93 class LoggingExecutor(Executor):
6194 def __init__(self, filename):
6295 self.filename = filename
6396 self.log = open(filename, 'w')
6497 print("logging to {}".format(self.filename))
6598
66 def do_it(self, cmd, **kwargs):
67 self.log.write('>>> {}\n'.format(cmd))
68 self.log.flush()
69 try:
70 check_call(cmd, shell=True, stdout=self.log, stderr=self.log, **kwargs)
71 except Exception as e:
72 self.log.close()
73 print(str(e))
74 check_call("tail %s" % self.filename, shell=True)
75 sys.exit(1)
99 def cleanup(self, exc):
100 print(str(exc))
101 check_call(["tail", self.filename])
102 sys.exit(1)
76103
77104 def close(self):
78105 self.log.close()
79
80
81 class Executor(object):
82 def do_it(self, cmd, **kwargs):
83 print(cmd)
84 check_call(cmd, shell=True, **kwargs)
85
86 def close(self):
87 pass
88106
89107
90108 def fmod(n, d):