git @ Cat's Eye Technologies kinoje / master src / kinoje / compiler.py
master

Tree @master (Download .tar.gz)

compiler.py @masterraw · history · blame

from argparse import ArgumentParser
import os
import sys

from kinoje.utils import BaseProcessor, load_config_file, zrange


SUPPORTED_OUTPUT_FORMATS = ('.m4v', '.mp4', '.gif')


class Compiler(BaseProcessor):
    def __init__(self, config, dirname, outfilename, **kwargs):
        super(Compiler, self).__init__(config, **kwargs)
        self.dirname = dirname
        self.outfilename = outfilename
        self.frame_fmt = "%08d.png"

    @classmethod
    def get_class_for(cls, filename):
        (whatever, outext) = os.path.splitext(filename)
        if outext not in SUPPORTED_OUTPUT_FORMATS:
            raise ValueError("%s not a supported output format (%r)" % (outext, SUPPORTED_OUTPUT_FORMATS))
        return {
            '.gif': GifCompiler,
            '.mp4': MpegCompiler,
            '.m4v': MpegCompiler,
        }[outext]

    def compile(self, num_frames):
        raise NotImplementedError

    def compile_all(self):
        tasks = [lambda: self.compile(self.config['num_frames'])]
        for task in self.tqdm(tasks):
            result = task()
        return result


class GifCompiler(Compiler):
    
    def compile(self, num_frames):
        # TODO: show some warning if this is not an integer delay
        delay = int(100.0 / self.config['fps'])

        filenames = [os.path.join(self.dirname, self.frame_fmt % f) for f in zrange(0, num_frames)]
        if self.config.get('shorten_final_frame'):
            filespec = ' '.join(filenames[:-1] + ['-delay', str(delay / 2), filenames[-1]])
        else:
            filespec = ' '.join(filenames)

        # -strip is there to force convert to process all input files.  (if no transformation is given,
        # it can sometimes stop reading input files.  leading to skippy animations.  who knows why.)
        self.exe.do_it(
            ["convert", "-delay", delay, "-loop", "0", filespec, "-strip", self.outfilename],
            shell=False
        )

    def view(self):
        self.exe.do_it(["eog", self.outfilename], shell=False)


class MpegCompiler(Compiler):

    def compile(self, num_frames):
        ifmt = os.path.join(self.dirname, self.frame_fmt)
        # fun fact: even if you say -r 30, ffmpeg still picks 25 fps
        self.exe.do_it(
            ["ffmpeg", "-i", ifmt, "-c:v", "libx264", "-profile:v", "baseline", "-pix_fmt", "yuv420p",
             "-r", str(int(self.config['fps'])), "-y", self.outfilename],
            shell=False
        )

    def view(self):
        self.exe.do_it(["vlc", self.outfilename], shell=False)


def main():
    argparser = ArgumentParser()

    argparser.add_argument('configfile', metavar='FILENAME', type=str,
        help='Configuration file containing the template and parameters'
    )
    argparser.add_argument('framesdir', metavar='DIRNAME', type=str,
        help='Directory that will be populated with image of each single frame'
    )
    argparser.add_argument('output', metavar='FILENAME', type=str,
        help='The movie file to create. The extension of this filename '
             'determines the output format and must be one of %r.  '
             'If not given, a default name will be chosen based on the '
             'configuration filename with a .mp4 extension added.' % (SUPPORTED_OUTPUT_FORMATS,)
    )

    argparser.add_argument("--shorten-final-frame", default=False, action='store_true',
        help="Make the last frame in a GIF animation delay only half as long. "
             "Might make looping smoother when uploaded to Twitter. YMMV."
    )
    argparser.add_argument("--view", default=False, action='store_true',
        help="Display the resultant movie."
    )
    argparser.add_argument('--version', action='version', version="%(prog)s 0.8")

    options = argparser.parse_args(sys.argv[1:])

    (whatever, outext) = os.path.splitext(options.output)
    if outext not in SUPPORTED_OUTPUT_FORMATS:
        raise ValueError("%s not a supported output format (%r)" % (outext, SUPPORTED_OUTPUT_FORMATS))

    config = load_config_file(options.configfile)
    config['shorten_final_frame'] = options.shorten_final_frame

    compiler = Compiler.get_class_for(options.output)(config, options.framesdir, options.output)
    compiler.compile_all()

    if options.view:
        compiler.view()