Пример #1
0
def title_screen():
    """
    Write the title on a black background
    @see https://ffmpeg.org/ffmpeg-filters.html#drawtext-1

    ffmpeg -f lavfi -i color=c=black:s=1920x1080:d=0.5 \
        -vf "drawtext=fontsize=30:fontcolor=white:fontfile=/path/font.ttf:text='Title':x=(w-text_w)/2:y=(h-text_h)/2" \
        -frames:v 1 output.png

    :return:
    """
    date = settings.TODAY.split('-')
    date = date[2] + '-' + date[1] + '-' + date[0]
    draw_text = 'drawtext=fontsize=' + str(
        settings.FONT_SIZE
    ) + ':fontcolor=white:fontfile=\'' + settings.FONT + '\''

    cmd = ffmpeg.get_command()

    cmd += '-f lavfi '
    cmd += '-i color=c=black:s=1920x1080:d=0.5 '
    cmd += '-vf "' + draw_text + ':text=\'' + date + '\':x=(w-text_w)/2:y=((h-text_h)/2)+50" '
    cmd += '-frames:v 1 ' + settings.DIR_DATA_DATE + '0.png'

    os.system(cmd)

    log.info('TITLE SCREEN SAVED.')
Пример #2
0
def cleanup(data, video_parts, credits_path):
    """
    Delete temporary files
    :param data:
    :param video_parts:
    :param credits_path:
    :return:
    """
    # delete rescaled images
    # (0.png, the title screen, is kept for archival)
    for i, word_data in data.items():
        if os.path.exists(settings.DIR_DATA_DATE + str(i + 1) + '-out.png'):
            os.remove(settings.DIR_DATA_DATE + str(i + 1) + '-out.png')
        elif os.path.exists(settings.DIR_DATA_DATE + str(i + 1) + '-out.gif'):
            os.remove(settings.DIR_DATA_DATE + str(i + 1) + '-out.gif')

    # delete temporary gif color palette file, if it exists
    if os.path.exists(settings.DIR_DATA_DATE + 'palette.png'):
        os.remove(settings.DIR_DATA_DATE + 'palette.png')

    # delete credits video
    os.remove(credits_path)

    # delete temporary lossless parts
    for part in video_parts:
        os.remove(part)

    log.info('TEMPORARY DATA DELETED.')
Пример #3
0
def get(url, word_idx):
    """
    using urlopen to avoid download errors and get content-type
    although APIs return image types
    without checking for HTTP errors, one could use 'urllib.request.urlretrieve(url, path)'
    :param url:
    :param word_idx:
    :return:
    """
    try:
        log.info('Requesting image: ' + str(word_idx + 1))
        req = urllib.request.urlopen(url)
    except urllib.request.URLError as e:
        log.warning('Image request failed ' + str(e.reason) + ' ' + url)
        return False
    except OSError as e:
        log.warning('Image request failed: ' + type(e).__name__ + ': ' +
                    str(e.strerror) + ' [' + str(e.errno) + '] ' + url)
        return False

    content_type = req.info()['Content-Type']
    extension = get_extension(content_type)

    if extension:
        return {
            'name': word_idx + 1,
            'image': req,
            'extension': extension,
            'type': content_type
        }

    return False
Пример #4
0
def run():

    from wdpkp import setup
    from wdpkp import requests
    from wdpkp.utils import log
    from wdpkp.movie import image
    from wdpkp.movie import selection
    from wdpkp.movie import ffmpeg
    from wdpkp.movie import text
    from wdpkp.utils import export
    # from wdpkp.movie import subtitles

    # Initialise
    setup.configure()

    log.info('PROGRAM START.')
    log.time('start')

    # Get input
    words = setup.get_words()

    # Make requests and parse responses
    data = requests.get(words)

    log.results(data, 'results-full')

    # Edit the movie
    data = selection.loop(data)

    log.results(data, 'results-selection')

    # adapt images to HD size
    image.resize_images(data)

    # make title & credit screens
    text.title_screen()
    credits_path = text.credit_screen(data)

    # srt file, will be read on movie editing
    # subtitles.create(data)

    # image > movie editing
    video_paths = ffmpeg.edit(data)

    # add the credits and subtitles
    master = ffmpeg.merge(video_paths, credits_path)
    # small = ffmpeg.merge_small(video_paths, credits_path)

    # delete temporary files
    setup.cleanup(data, video_paths, credits_path)

    # copy video to HTTP server
    export.send(master)
    # export.send(small)

    log.info('ALL DONE!')
    log.time('end')
Пример #5
0
def credit_screen(data):
    """
    Build ffmpeg command for the rolling credit screen:
    for each line append a drawtext command
    @see http://stackoverflow.com/questions/11058479/ffmpeg-how-does-moving-overlay-text-command-work

    ffmpeg -y -f lavfi -i color=c=black:s=1920x1080:d=10 \
        -vf drawtext="fontfile='/path/font.ttf':text='Rolling':fontsize=20:fontcolor=white:x=(w-text_w)/2:y=h-20*t" \
        output.mp4

    :return:
    """

    log.info('CREATING ROLLING CREDITS...')

    data = _credit_string_list(data)
    credits_total = len(data)
    count = -1
    duration = 96

    cmd = ffmpeg.get_command()
    cmd += ' -f lavfi -i color=c=black:s=1920x1080:r=24:d=' + str(
        duration) + ' -vf "'

    draw_txt_cmds = []

    for i in range(0, credits_total):
        count += 1
        credit_sub_lines = []
        for j in range(0, len(data[i])):
            count += 1
            credit_sub_lines.append(_credit_line(count, data[i][j]))

        draw_txt_cmds.append(','.join(credit_sub_lines))

    # append last line after a space
    # 'concept Tessa Groenewoud; code Arnaud Coolsaet'
    draw_txt_cmds.append(_credit_line_last(count))

    # join all the drawtext commands
    cmd += ','.join(draw_txt_cmds) + '" '

    # append output path '{today}/{last-frame-number}.mp4'
    credits_path = settings.DIR_DATA_DATE + 'credits.mp4'

    # lossless H264 export
    cmd += '-an -c:v libx264 -r 24 -preset ultrafast -qp 0 ' + credits_path

    # using unicode in drawtext text
    subprocess.call(cmd.encode('utf8'), shell=True)

    log.info('ROLLING CREDITS SAVED.')

    return credits_path
Пример #6
0
def configure():
    env_path = settings.DIR + '/.env'
    load_dotenv(dotenv_path=env_path)

    os.mkdir(settings.DIR_DATA_DATE)
    os.mkdir(settings.DIR_VIDEO_DATE)

    log.configure()

    settings.URL_BLACKLIST = re.compile(settings.URL_BLACKLIST)

    log.info('PROGRAM CONFIG OK.')
Пример #7
0
def save(downloaded):
    """
    Saves image as 'idx.ext' based on HTTP Content-Type
    :param downloaded:
    :return:
    """
    log.info('Saving image: ' + str(downloaded['name']) +
             downloaded['extension'])
    path = settings.DIR_DATA_DATE + str(
        downloaded['name']) + downloaded['extension']
    with open(path, 'wb') as output:
        output.write(downloaded['image'].read())
    return path
Пример #8
0
def merge(video_parts, credits_video):
    """
    Merge video, credit video + 3s of black.

    Previously subtitles were added here through an external .srt file,
    by the sync was perfect (some timings being very short), the subtitles
    are now burnt in each separate image. For the record, previous filter_complex:

    -filter_complex "[0:v][1:v][2:v] concat=n=3:v=1[v0];\
     [v0]subtitles=filename=/subtitles.srt:fontsdir=/path/:force_style=\'FontName=Helvetica Neue\,FontSize=16\'[v1]" \
     -map "[v1]"

    :param video_parts:
    :param credits_video:
    :return:
    """
    time = datetime.datetime.now()
    # subtitle_file = settings.DIR_VIDEO_DATE + 'wdpkp-' + settings.TODAY + '.srt'
    filename = settings.DIR_VIDEO_DATE + 'wdpkp-' + settings.TODAY + '.mp4'

    log.info('MERGING VIDEO PARTS...')

    cmd = get_command()

    # concatenate the video parts
    num_parts = len(video_parts)
    stream_ids = ''
    for i, part in enumerate(video_parts):
        stream_ids += '[' + str(i) + ':v]'
        cmd += ' -i "' + part + '"'

    # add credits + 3s of black
    # add metadata
    stream_ids += '[' + str(num_parts) + ':v][' + str(num_parts + 1) + ':v]'
    cmd += ' -i "' + credits_video + '"' \
           + ' -f lavfi -i "color=c=black:s=1920x1080:r=24:d=3" ' \
           + ' -filter_complex "' + stream_ids + ' concat=n=' + str(num_parts + 2) + ':v=1[v1]"' \
           + ' -map "[v1]"' \
           + ' -metadata title="' + settings.TITLE + '" -metadata year="' + str(time.year) + '"' \
           + ' -metadata author="Tessa Groenewoud" ' \
           + ' -an -pix_fmt yuv420p -r 24 -movflags faststart ' + filename

    # + '[v0]subtitles=filename=' + subtitle_file + ':fontsdir=' \
    # + settings.FONT_DIR + ':force_style=\'FontName=Helvetica Neue\,FontSize=16\'[v1]"' \

    os.system(cmd)

    log.info('MASTER VIDEO SAVED : ' + filename)

    return filename
Пример #9
0
def get(words):
    """
    Call APIs synchronously,
    re-invoke the method if requests failed
    :param words:
    :return:
    """
    global times_relaunched
    failed = 0

    for i, val in words.items():

        # only continue for previously failed requests
        if 'api' in words[i]:
            continue

        # choose a controller
        controller = _choose_controller(val['word'])

        # construct the URL param
        url = controller.get_url(val['word'])
        headers = controller.get_http_headers()

        # make the request
        response = _make_request(url, headers)

        # handle response
        if response:
            controller_name = controller.__name__.split('.')[-1]
            # get the returned image URLs
            parsed = controller.parse(response)
            if parsed:
                words[i]['api'] = controller_name
                words[i]['urls'] = parsed
            else:
                log.warning('Nothing parsed from ' + controller_name)
                failed = 1
        else:
            # failed HTTP requests are logged above
            failed = 1

    # re-request for failed responses
    if failed and settings.ALLOW_RELAUNCH and settings.RELAUNCH_TIMES >= times_relaunched:
        times_relaunched += 1
        log.info('Relaunching failed requests...')
        get(words)

    return words
Пример #10
0
def _make_request(url, headers):
    """
    Handle API requests and errors
    :param url:
    :param headers:
    :return:
    """
    log.info('FETCHING: ' + url)
    req = urllib.request.Request(url, None, headers)

    try:
        req = urllib.request.urlopen(req)
        encoding = req.info().get_content_charset('utf-8')
        data = req.read()
        return json.loads(data.decode(encoding))
    except urllib.request.URLError as e:
        log.warning('REQUEST FAILED: ' + str(e.reason))
        return False
Пример #11
0
def _edit_part(data, part):
    """
    Our main still image merging function
    :param data:
    :return:
    """
    time_codes = get_time_codes()
    video_path = settings.DIR_VIDEO_DATE + 'wdpkp-' + settings.TODAY + '-part-' + part + '.mp4'
    num_input_streams = len(data.items())

    log.info('START EDITING VIDEO PART ' + part + '...')

    cmd = get_command()

    if part == '1':
        # extract title image stream
        num_input_streams += 1
        cmd += _img_demux(time_codes[0], settings.DIR_DATA_DATE + '0.png')

    for i, word_data in data.items():

        if word_data['type'] != 'image/gif':
            filename = settings.DIR_DATA_DATE + str(i + 1) + '-out.png'
            # extract image stream, for non gif images
            cmd += _img_demux(time_codes[i + 1], filename)
        else:
            filename = settings.DIR_DATA_DATE + str(i + 1) + '-out.gif'
            # extract image stream, for gif images
            cmd += _img_demux_gif(time_codes[i + 1], filename)

    # says: 'stitch all images together and map to one video stream'
    cmd += "-filter_complex 'concat=n=" + str(
        num_input_streams) + ":v=1[v]' -map '[v]' "

    # lossless H264 encoding
    cmd += "-an -c:v libx264 -preset ultrafast -qp 0 -pix_fmt yuv420p -r 24 " + video_path

    os.system(cmd)

    log.info('VIDEO PART ' + part + ' EDITED & SAVED.')

    return video_path
Пример #12
0
def merge_small(video_parts, credits_video):
    time = datetime.datetime.now()
    filename = settings.DIR_VIDEO_DATE + 'wdpkp-' + settings.TODAY + '-320x180.mp4'

    log.info('MERGING SMALL VIDEO...')

    cmd = get_command()

    # concatenate the video parts
    num_parts = len(video_parts)
    stream_ids = ''
    scale_filters = ''

    for i, part in enumerate(video_parts):
        scale_filters += '[' + str(i) + ':v]scale=320x180[v' + str(i) + '];'
        stream_ids += '[v' + str(i) + ']'
        cmd += ' -i "' + part + '"'

    # add credits + 3s of black
    # add metadata
    scale_filters += '[' + str(num_parts) + ':v]scale=320x180[v' + str(
        num_parts) + '];'
    stream_ids += '[v' + str(num_parts) + ']'
    stream_ids += '[' + str(num_parts + 1) + ':v]'
    cmd += ' -i "' + credits_video + '"' \
           + ' -f lavfi -i "color=c=black:s=320x180:r=24:d=3" ' \
           + ' -filter_complex "' + scale_filters + stream_ids + 'concat=n=' + str(num_parts + 2) + ':v=1[v1]"' \
           + ' -map "[v1]"' \
           + ' -metadata title="' + settings.TITLE + '" -metadata year="' + str(time.year) + '"' \
           + ' -metadata author="Tessa Groenewoud" ' \
           + ' -an -pix_fmt yuv420p -r 24 -crf 18 -movflags faststart ' + filename

    os.system(cmd)

    log.info('SMALL VIDEO SAVED : ' + filename)

    return filename
Пример #13
0
def send(file):
    log.info('EXPORTING VIDEO...')
    os.system('scp -q ' + file + ' ' + os.getenv("EXPORT_USER") + '@' +
              os.getenv("EXPORT_HOST") + ':' + os.getenv("EXPORT_DIR"))
Пример #14
0
def resize_images(data):
    """
    Resize and fit each image in a HD frame, by padding it with black where necessary,
    this makes the subsequent ffmpeg command to create the full video much simpler.
    The subtitle text is also burned in here per image (keeping the sync between image and
    text perfect. Because some pictures have very short timings, ffmpeg couldn't synchronize
    perfectly using a subtitle file, which was merged on the full video.)

    Several quality tests were done, jpg had color space issues (black wasn't black),
    tiff is quite heavy to process... function uses png, below are the test commands:

    ## png > +/- 3.1MB
    ffmpeg -i in.jpg -filter:v "scale=iw*min(1920/iw\,1080/ih):ih*min(1920/iw\,1080/ih), \
        pad=1920:1080:(1920-iw*min(1920/iw\,1080/ih))/2:(1080-ih*min(1920/iw\,1080/ih))/2:black, \
        setsar=1/1, setdar=16/9" \
        out.png

    ## jpg > quality -q:v 1 (max) - 31 (min) > padded black is not 100% black
    ffmpeg -i in.jpg -filter:v "scale=iw*min(1920/iw\,1080/ih):ih*min(1920/iw\,1080/ih), \
        pad=1920:1080:(1920-iw*min(1920/iw\,1080/ih))/2:(1080-ih*min(1920/iw\,1080/ih))/2:0x000000, \
        setsar=1/1, setdar=16/9" \
        -q:v 1 -pix_fmt yuvj420p out.jpg

    ## jpg > better
    ffmpeg -f lavfi -i color=c=black:s=1920x1080 -i in.jpg \
        -filter_complex "[1:v]scale=..., setsar=1/1, setdar=16/9[ovr];[ovr]blend=all_mode='normal'[v]" \
        -map '[v]' -q:v 1 -pix_fmt yuvj420p out.jpg

    ## tiff > best > +/- 6MB
    ffmpeg -i in.jpg -filter:v "scale=..., setsar=1/1, setdar=16/9" -compression_algo raw -pix_fmt rgb24 out.tiff
    @see http://superuser.com/questions/881783/convert-from-avi-to-uncompressed-tiff-using-ffmpeg

    :param data:
    :return:
    """
    draw_text = 'drawtext=fontsize=40:fontcolor=white:borderw=2:fontfile=\'' + settings.FONT_BOLD + '\''
    cmd = ''

    log.info('RESCALING IMAGES...')

    for i, word_data in data.items():

        if word_data['type'] == 'image/gif':
            # save as gif
            filename = settings.DIR_DATA_DATE + str(i + 1) + '-out.gif'
            cmd += _resize_gif_cmd(word_data['file']) \
                + '[v0]' + draw_text + ':text=\'' + word_data['punc'] + '\':x=(w-text_w)/2:y=(h-110)[v1]"' \
                + ' -map "[v1]" -pix_fmt rgb8 ' + filename + ';'

        else:
            # save as png
            filename = settings.DIR_DATA_DATE + str(i + 1) + '-out.png'
            cmd += ffmpeg.get_command() \
                   + ' -i ' + word_data['file'] \
                   + ' -filter_complex "' + _get_scale_args() + '[v0];' \
                   + '[v0]' + draw_text + ':text=\'' + word_data['punc'] + '\':x=(w-text_w)/2:y=(h-110)[v1]"' \
                   + ' -map "[v1]" -pix_fmt rgb24 ' + filename + ';'

        # store tmp path
        data[i].update({'tmp': filename})

    # run all rescaling commands at once
    subprocess.call(cmd.encode('utf8'), shell=True)

    log.info('IMAGES RESCALED.')