git @ Cat's Eye Technologies kinoje / 157abf4
Major refactor. Split into separate executables. Chris Pressey 2 years ago
12 changed file(s) with 384 addition(s) and 231 deletion(s). Raw diff Collapse all Expand all
00 kinoje
11 ======
22
3 *Version 0.1. Subject to backwards-incompatible changes without notice.*
3 *Version 0.2. Subject to backwards-incompatible changes without notice.*
44
55 **kinoje** is a templating-based animation tool. A provided template is filled out once for each
66 frame of the animation; the result of the template expansion is used to create a still image; and
44
55 sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
66
7 from kinoje import main
7 from kinoje import orchestrator
88
99
1010 if __name__ == '__main__':
11 main.main()
11 orchestrator.main()
0 #!/usr/bin/env python
1
2 from os.path import realpath, dirname, join
3 import sys
4
5 sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
6
7 from kinoje import compiler
8
9
10 if __name__ == '__main__':
11 compiler.main()
0 #!/usr/bin/env python
1
2 from os.path import realpath, dirname, join
3 import sys
4
5 sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
6
7 from kinoje import expander
8
9
10 if __name__ == '__main__':
11 expander.main()
0 #!/usr/bin/env python
1
2 from os.path import realpath, dirname, join
3 import sys
4
5 sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
6
7 from kinoje import renderer
8
9
10 if __name__ == '__main__':
11 renderer.main()
00 duration: 1.44
1 start: 0.0
2 stop: 1.0
3 fps: 25.0
4 width: 320.0
5 height: 200.0
6 command_template: povray +L{indir} -D +I{infile} +O{outfile} +W{width} +H{height} +A
17 template: |-
28 global_settings { assumed_gamma 2.2 }
39
0 type: svg
0 command_template: inkscape -z -e {outfile} -w {width} -h {height} {infile}
11 template: |-
22 <?xml version="1.0" standalone="no"?>
33 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
0 from argparse import ArgumentParser
01 import os
2 import sys
3
4 import yaml
5 try:
6 from yaml import CLoader as Loader
7 except ImportError:
8 from yaml import Loader
9
10 from kinoje.utils import LoggingExecutor
11 from kinoje.expander import load_config_file
12
13
14 SUPPORTED_OUTPUT_FORMATS = ('.m4v', '.mp4', '.gif')
115
216
317 class Compiler(object):
4 def __init__(self, dirname, outfilename, options, exe):
18 def __init__(self, dirname, outfilename, config, exe):
519 self.dirname = dirname
6 self.options = options
720 self.exe = exe
821 self.outfilename = outfilename
22 self.config = config
23 self.frame_fmt = "%08d.png"
924
1025
1126 class GifCompiler(Compiler):
1227
1328 def compile(self, num_frames):
1429 # TODO: show some warning if this is not an integer delay
15 delay = int(100.0 / self.options.fps)
30 delay = int(100.0 / self.config['fps'])
1631
17 filenames = [os.path.join(self.dirname, self.options.frame_fmt % f) for f in xrange(0, num_frames)]
18 if self.options.shorten_final_frame:
32 filenames = [os.path.join(self.dirname, self.frame_fmt % f) for f in xrange(0, num_frames)]
33 if self.config.get('shorten_final_frame'):
1934 filespec = ' '.join(filenames[:-1] + ['-delay', str(delay / 2), filenames[-1]])
2035 else:
2136 filespec = ' '.join(filenames)
3348 class MpegCompiler(Compiler):
3449
3550 def compile(self, num_frames):
36 ifmt = os.path.join(self.dirname, self.options.frame_fmt)
51 ifmt = os.path.join(self.dirname, self.frame_fmt)
3752 # fun fact: even if you say -r 30, ffmpeg still picks 25 fps
3853 cmd = "ffmpeg -i %s -c:v libx264 -profile:v baseline -pix_fmt yuv420p -r %s -y %s" % (
39 ifmt, int(self.options.fps), self.outfilename
54 ifmt, int(self.config['fps']), self.outfilename
4055 )
4156 self.exe.do_it(cmd)
4257
4358 def view(self):
4459 self.exe.do_it("vlc %s" % self.outfilename)
60
61
62 def main():
63 argparser = ArgumentParser()
64
65 argparser.add_argument('configfile', metavar='FILENAME', type=str,
66 help='Configuration file containing the template and parameters'
67 )
68 argparser.add_argument('framesdir', metavar='DIRNAME', type=str,
69 help='Directory that will be populated with image of each single frame'
70 )
71 argparser.add_argument('output', metavar='FILENAME', type=str,
72 help='The movie file to create. The extension of this filename '
73 'determines the output format and must be one of %r. '
74 'If not given, a default name will be chosen based on the '
75 'configuration filename with a .mp4 extension added.' % (SUPPORTED_OUTPUT_FORMATS,)
76 )
77
78 #argparser.add_argument("--still", default=None, type=float, metavar='INSTANT',
79 # help="If given, generate only a single frame (at the specified instant "
80 # "betwen 0.0 and 1.0) and display it using eog, instead of building "
81 # "the whole movie."
82 #)
83 #argparser.add_argument("--view", default=False, action='store_true')
84 #argparser.add_argument("--shorten-final-frame", default=False, action='store_true',
85 # help="Make the last frame in a GIF animation delay only half as long. "
86 # "Might make looping smoother when uploaded to Twitter. YMMV."
87 #)
88
89 #if options.still is not None:
90 # exe.do_it("eog %s" % fn)
91 # sys.exit(0)
92
93 options = argparser.parse_args(sys.argv[1:])
94
95 (whatever, outext) = os.path.splitext(options.output)
96 if outext not in SUPPORTED_OUTPUT_FORMATS:
97 raise ValueError("%s not a supported output format (%r)" % (outext, SUPPORTED_OUTPUT_FORMATS))
98
99 config = load_config_file(options.configfile)
100
101 exe = LoggingExecutor(os.path.join(options.framesdir, 'movie.log'))
102
103 compiler = {
104 '.gif': GifCompiler,
105 '.mp4': MpegCompiler,
106 '.m4v': MpegCompiler,
107 }[outext](options.framesdir, options.output, config, exe)
108
109 compiler.compile(config['num_frames'])
110
111 #if options.view:
112 # compiler.view()
113
114 exe.close()
0 from argparse import ArgumentParser
1 from copy import copy
2 import math
3 import os
4 import sys
5
6 from jinja2 import Template
7
8 from kinoje.utils import LoggingExecutor, fmod, tween
9
10
11 def load_config_file(filename):
12 import yaml
13 try:
14 from yaml import CLoader as Loader
15 except ImportError:
16 from yaml import Loader
17
18 with open(filename, 'r') as file_:
19 config = yaml.load(file_, Loader=Loader)
20
21 template = Template(config['template'])
22 config['start'] = float(config.get('start', 0.0))
23 config['stop'] = float(config.get('stop', 1.0))
24 config['fps'] = float(config.get('fps', 25.0))
25 config['width'] = float(config.get('width', 320.0))
26 config['height'] = float(config.get('height', 200.0))
27
28 duration = config['duration']
29 start = config['start']
30 stop = config['stop']
31 fps = config['fps']
32
33 config['start_time'] = start * duration
34 config['stop_time'] = stop * duration
35 config['requested_duration'] = config['stop_time'] - config['start_time']
36 config['num_frames'] = int(config['requested_duration'] * fps)
37 config['t_step'] = 1.0 / (duration * fps)
38
39 return config
40
41
42 class Expander(object):
43 """Takes a directory and a template (Jinja2) and expands the template a number of times,
44 creating a number of filled-out text files in the directory."""
45 def __init__(self, dirname, template, config, exe):
46 self.dirname = dirname
47 self.template = template
48 self.config = config
49 self.exe = exe
50
51 self.fun_context = {}
52 for key, value in self.config.get('functions', {}).iteritems():
53 self.fun_context[key] = eval("lambda x: " + value)
54
55 def fillout_template(self, frame, t):
56 context = copy(self.config)
57 context.update(self.fun_context)
58 context.update({
59 'width': float(self.config.get('width', 320.0)),
60 'height': float(self.config.get('height', 200.0)),
61 't': t,
62 'math': math,
63 'tween': tween,
64 'fmod': fmod,
65 })
66 output_filename = os.path.join(self.dirname, "%08d.txt" % frame)
67 with open(output_filename, 'w') as f:
68 f.write(self.template.render(context))
69
70
71 def main():
72 argparser = ArgumentParser()
73
74 argparser.add_argument('configfile', metavar='FILENAME', type=str,
75 help='Configuration file containing the template and parameters'
76 )
77 argparser.add_argument('instantsdir', metavar='DIRNAME', type=str,
78 help='Directory that will be populated with instants (text files describing frames)'
79 )
80
81 options = argparser.parse_args(sys.argv[1:])
82
83 config = load_config_file(options.configfile)
84
85 exe = LoggingExecutor('movie.log')
86
87 expander = Expander(options.instantsdir, template, config, exe)
88
89 t = config['start']
90 t_step = config['t_step']
91 for frame in xrange(num_frames):
92 expander.fillout_template(frame, t)
93 t += t_step
94
95 exe.close()
+0
-190
src/kinoje/main.py less more
0 """\
1 kinoje {options} input-file.yaml output-file.{gif,m4v,mp4}
2
3 Create a movie file from the template and configuration in the given YAML file."""
4
5 # Note: just about everything here is subject to change!
6
7 from datetime import datetime, timedelta
8 from argparse import ArgumentParser
9 import os
10 import re
11 import sys
12 from tempfile import mkdtemp
13
14 from jinja2 import Template
15 import yaml
16 try:
17 from yaml import CLoader as Loader
18 except ImportError:
19 from yaml import Loader
20
21 from kinoje.utils import LoggingExecutor
22 from kinoje.renderer import Renderer
23 from kinoje.compiler import GifCompiler, MpegCompiler
24
25
26 SUPPORTED_OUTPUT_FORMATS = ('.m4v', '.mp4', '.gif')
27
28
29 def main():
30 argparser = ArgumentParser()
31
32 argparser.add_argument('configfile', metavar='FILENAME', type=str,
33 help='A YAML file containing the template to render for each frame, '
34 'as well as configuration for rendering the template.'
35 )
36 argparser.add_argument('-o', '--output', metavar='FILENAME', type=str, default=None,
37 help='The movie file to create. The extension of this filename '
38 'determines the output format and must be one of %r. '
39 'If not given, a default name will be chosen based on the '
40 'configuration filename with a .mp4 extension added.' % (SUPPORTED_OUTPUT_FORMATS,)
41 )
42
43 argparser.add_argument("--width", default=320, type=int)
44 argparser.add_argument("--height", default=200, type=int)
45
46 argparser.add_argument("--size", default=None, type=str,
47 help=''
48 )
49
50 argparser.add_argument("--square", default=False, action='store_true')
51
52 argparser.add_argument("--start", default=0.0, type=float, metavar='INSTANT',
53 help="t-value at which to start rendering the movie. Default=0.0"
54 )
55 argparser.add_argument("--stop", default=1.0, type=float, metavar='INSTANT',
56 help="t-value at which to stop rendering the movie. Default=1.0"
57 )
58 argparser.add_argument("--duration", default=None, type=float, metavar='SECONDS',
59 help="Override the duration specified in the configuration."
60 )
61
62 argparser.add_argument("--fps", default=25.0, type=float, metavar='FPS',
63 help="The number of frames to render for each second. Note that the "
64 "tool that makes a movie file from images might not honour this value exactly."
65 )
66 argparser.add_argument("--frame-fmt", default="out%05d.png", type=str)
67
68 argparser.add_argument("--still", default=None, type=float, metavar='INSTANT',
69 help="If given, generate only a single frame (at the specified instant "
70 "betwen 0.0 and 1.0) and display it using eog, instead of building "
71 "the whole movie."
72 )
73 argparser.add_argument("--view", default=False, action='store_true')
74 argparser.add_argument("--shorten-final-frame", default=False, action='store_true',
75 help="Make the last frame in a GIF animation delay only half as long. "
76 "Might make looping smoother when uploaded to Twitter. YMMV."
77 )
78
79 argparser.add_argument("--config", default=None, type=str)
80
81 options = argparser.parse_args(sys.argv[1:])
82
83 options.width, options.height = {
84 'tiny': (160, 100),
85 'small': (320, 200),
86 'big': (640, 400),
87 'huge': (800, 600),
88 'giant': (1280, 800),
89 }[options.size] if options.size is not None else (options.width, options.height)
90
91 if options.square:
92 options.height = options.width
93
94 if options.still is not None:
95 options.duration = 1.0
96 options.start = options.still
97
98 infilename = options.configfile
99 outfilename = options.output
100 if options.output is None:
101 (inbase, inext) = os.path.splitext(os.path.basename(infilename))
102 outfilename = inbase + '.mp4'
103 (whatever, outext) = os.path.splitext(outfilename)
104 if outext not in SUPPORTED_OUTPUT_FORMATS:
105 raise ValueError("%s not a supported output format (%r)" % (outext, SUPPORTED_OUTPUT_FORMATS))
106
107 with open(infilename, 'r') as file_:
108 config = yaml.load(file_, Loader=Loader)
109
110 if options.config is not None:
111 settings = {}
112 for setting_string in options.config.split(','):
113 key, value = setting_string.split(':')
114 if re.match(r'^\d*\.?\d*$', value):
115 value = float(value)
116 settings[key] = value
117 config.update(settings)
118
119 template = Template(config['template'])
120
121 tempdir = mkdtemp()
122
123 duration = options.duration
124 if duration is None:
125 duration = config['duration']
126
127 if 'render_command_template' not in config:
128 render_type = config.get('type', 'povray')
129 if render_type == 'povray':
130 config['render_command_template'] = "povray +L{indir} -D +I{infile} +O{outfile} +W{width} +H{height} +A"
131 elif render_type == 'svg':
132 config['render_command_template'] = "inkscape -z -e {outfile} -w {width} -h {height} {infile}"
133 else:
134 raise NotImplementedError
135
136 start_time = options.start * duration
137 stop_time = options.stop * duration
138 requested_duration = stop_time - start_time
139 num_frames = int(requested_duration * options.fps)
140 t_step = 1.0 / (duration * options.fps)
141
142 print "Start time: t=%s, %s seconds" % (options.start, start_time)
143 print "Stop time: t=%s, %s seconds" % (options.stop, stop_time)
144 print "Requested duration: %s seconds" % requested_duration
145 print "Frame rate: %s fps" % options.fps
146 print "Number of frames: %s (rounded to %s)" % (requested_duration * options.fps, num_frames)
147 print "t-Step: %s" % t_step
148
149 exe = LoggingExecutor(os.path.join(tempdir, 'movie.log'))
150 t = options.start
151
152 started_at = datetime.now()
153
154 renderer = Renderer(tempdir, template, config, options, exe)
155
156 for frame in xrange(num_frames):
157
158 elapsed = (datetime.now() - started_at).total_seconds()
159 eta = '???'
160 if frame > 0:
161 seconds_per_frame = elapsed / float(frame)
162 eta = started_at + timedelta(seconds=num_frames * seconds_per_frame)
163
164 print "t=%s (%s%% done, eta %s)" % (t, int(((t - options.start) / options.stop) * 100), eta)
165
166 fn = renderer.render_frame(frame, t)
167
168 t += t_step
169
170 if options.still is not None:
171 exe.do_it("eog %s" % fn)
172 sys.exit(0)
173
174 compiler = {
175 '.gif': GifCompiler,
176 '.mp4': MpegCompiler,
177 '.m4v': MpegCompiler,
178 }[outext](tempdir, outfilename, options, exe)
179
180 compiler.compile(num_frames)
181 finished_at = datetime.now()
182
183 if options.view:
184 compiler.view()
185
186 exe.close()
187
188 run_duration = finished_at - started_at
189 print "Finished, took %s seconds" % run_duration.total_seconds()
0 """\
1 kinoje {options} input-file.yaml output-dir/
2
3 Create a sequence of text file descriptions of frames of a movie from the
4 configuration (which incl. a master template) in the given YAML file."""
5
6 # Note: just about everything here is subject to change!
7
8 from datetime import datetime, timedelta
9 from argparse import ArgumentParser
10 import os
11 import re
12 import sys
13 from tempfile import mkdtemp
14
15 from jinja2 import Template
16 import yaml
17 try:
18 from yaml import CLoader as Loader
19 except ImportError:
20 from yaml import Loader
21
22 from kinoje.utils import LoggingExecutor
23
24
25 def main():
26 argparser = ArgumentParser()
27
28 argparser.add_argument('configfile', metavar='FILENAME', type=str,
29 help='A YAML file containing the template to render for each frame, '
30 'as well as configuration for rendering the template.'
31 )
32
33 infilename = options.configfile
34 outfilename = options.output
35 if options.output is None:
36 (inbase, inext) = os.path.splitext(os.path.basename(infilename))
37 outfilename = inbase + '.mp4'
38 (whatever, outext) = os.path.splitext(outfilename)
39 if outext not in SUPPORTED_OUTPUT_FORMATS:
40 raise ValueError("%s not a supported output format (%r)" % (outext, SUPPORTED_OUTPUT_FORMATS))
41
42 with open(infilename, 'r') as file_:
43 config = yaml.load(file_, Loader=Loader)
44
45 if options.config is not None:
46 settings = {}
47 for setting_string in options.config.split(','):
48 key, value = setting_string.split(':')
49 if re.match(r'^\d*\.?\d*$', value):
50 value = float(value)
51 settings[key] = value
52 config.update(settings)
53
54 template = Template(config['template'])
55
56 text_files_dir = mkdtemp()
57 images_dir = mkdtemp()
58
59 duration = options.duration
60 if duration is None:
61 duration = config['duration']
62
63 if 'render_command_template' not in config:
64 render_type = config.get('type', 'povray')
65 if render_type == 'povray':
66 config['render_command_template'] = "povray +L{indir} -D +I{infile} +O{outfile} +W{width} +H{height} +A"
67 elif render_type == 'svg':
68 config['render_command_template'] = "inkscape -z -e {outfile} -w {width} -h {height} {infile}"
69 else:
70 raise NotImplementedError
71
72 start_time = options.start * duration
73 stop_time = options.stop * duration
74 requested_duration = stop_time - start_time
75 num_frames = int(requested_duration * options.fps)
76 t_step = 1.0 / (duration * options.fps)
77
78 print "Start time: t=%s, %s seconds" % (options.start, start_time)
79 print "Stop time: t=%s, %s seconds" % (options.stop, stop_time)
80 print "Requested duration: %s seconds" % requested_duration
81 print "Frame rate: %s fps" % options.fps
82 print "Number of frames: %s (rounded to %s)" % (requested_duration * options.fps, num_frames)
83 print "t-Step: %s" % t_step
84
85 exe = LoggingExecutor(os.path.join(text_files_dir, 'movie.log'))
86 t = options.start
87
88 started_at = datetime.now()
89
90 expander = TemplateExpander(text_files_dir, template, config, options, exe)
91 for frame in xrange(num_frames):
92 expander.fillout_template(frame, t)
93 t += t_step
94
95 exe.close()
96
97 run_duration = finished_at - started_at
98 print "Finished, took %s seconds" % run_duration.total_seconds()
0 from copy import copy
1 import math
0 from argparse import ArgumentParser
1 import re
22 import os
3 import sys
34
4 from kinoje.utils import fmod, tween
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 LoggingExecutor
12
13
14 SUPPORTED_OUTPUT_FORMATS = ('.m4v', '.mp4', '.gif')
515
616
717 class Renderer(object):
8 def __init__(self, dirname, template, config, options, exe):
9 self.dirname = dirname
10 self.template = template
11 self.config = config
12 self.options = options
18 """Takes a source directory filled with text files and a destination directory and
19 creates one image file in the destination directory from each text file in the source."""
20 def __init__(self, command_template, src, dest, exe, width=320, height=200):
21 self.command_template = command_template
22 self.src = src
23 self.dest = dest
1324 self.exe = exe
25 self.width = width
26 self.height = height
1427
15 self.fun_context = {}
16 for key, value in self.config.get('functions', {}).iteritems():
17 self.fun_context[key] = eval("lambda x: " + value)
28 def render_all(self):
29 for filename in sorted(os.listdir(self.src)):
30 full_srcname = os.path.join(self.src, filename)
31 match = re.match(r'^.*?(\d+).*?$', filename)
32 frame = int(match.group(1))
33 destname = "%08d.png" % frame
34 full_destname = os.path.join(self.dest, destname)
35 self.render(frame, full_srcname, full_destname)
1836
19 def render_frame(self, frame, t):
20 out_pov = os.path.join(self.dirname, 'out.pov')
21 context = copy(self.config)
22 context.update(self.fun_context)
23 context.update({
24 'width': float(self.options.width),
25 'height': float(self.options.height),
26 't': t,
27 'math': math,
28 'tween': tween,
29 'fmod': fmod,
30 })
31 with open(out_pov, 'w') as f:
32 f.write(self.template.render(context))
33 fn = os.path.join(self.dirname, self.options.frame_fmt % frame)
34 cmd = self.config['render_command_template'].format(
35 infile=out_pov, indir=os.path.dirname(self.options.configfile), outfile=fn,
36 width=self.options.width, height=self.options.height
37 def render(self, frame, full_srcname, full_destname):
38 cmd = self.command_template.format(
39 infile=full_srcname,
40 indir=self.src,
41 outfile=full_destname,
42 width=self.width,
43 height=self.height
3744 )
3845 self.exe.do_it(cmd)
39 return fn
46
47
48 def main():
49 argparser = ArgumentParser()
50
51 argparser.add_argument('configfile', metavar='FILENAME', type=str,
52 help='Configuration file containing the template and parameters'
53 )
54 argparser.add_argument('instantsdir', metavar='DIRNAME', type=str,
55 help='Directory containing instants (text file descriptions of each single frame) to render'
56 )
57 argparser.add_argument('framesdir', metavar='DIRNAME', type=str,
58 help='Directory that will be populated with images, one for each frame'
59 )
60
61 options = argparser.parse_args(sys.argv[1:])
62
63 with open(options.configfile, 'r') as file_:
64 config = yaml.load(file_, Loader=Loader)
65
66 exe = LoggingExecutor('movie.log')
67
68 command_template = '???'
69 renderer = Renderer(config['command_template'], options.instantsdir, options.framesdir, exe)
70 renderer.render_all()
71
72 exe.close()
73
74 #run_duration = finished_at - started_at
75 #print "Finished, took %s seconds" % run_duration.total_seconds()