VTuberの切り抜きを簡単に作りたい!PythonでYouTube動画を切り取るツールを作ってみた。

Python
スポンサードリンク

皆さんYouTubeは見ますか?
筆者は毎日のように見ています。
というかながら見とかしてるのでPCの前に居る時はほぼ確実に見ていますね。

今回はそんなYouTube中毒になってしまった筆者が「VTuber動画の切り抜きを簡単にやりたい!」というわがままを技術で解決すべく作った切り抜きツールの備忘録的な記事です。

YouTubeの動画を楽に切り抜きたい!

「VTuberの配信のおもしろシーンをとにかく楽して切り抜きたい!」と誰しもが思ったことがあると思います。

そもそもは

筆者「スマホとかのモバイル端末で動画編集するのはめんどいなぁ・・・」

筆者「せや、ブラウザから簡単に切り抜けるいい感じのページを作ろ!」

という感じで作り始めました。

YouTubeの動画をとにかく楽して切り抜くためにPythonでなんかいい感じのライブラリがないか探ってみます。

動画をダウンロード→切り抜き→動画を出力

今回やりたいのは単純で、動画をダウンロードして指定した時間を切り抜いてから再度動画にエンコードするというもの。

配信のアーカイブなんかの長い動画から数十秒、数分の動画を切り抜くために動画全体をダウンロードするので正直効率は微妙です。

ですが、今回は家で筆者のみが使うシステムなのでそのへんの効率とかは度外視します。

とにかく動画を簡単にダウンロードする

とにかく簡単に動画をダウンロードするために、Pythonでサクッと動画をダウンロードできるライブラリを探していると「youtube-dl」といういかにもダウンロードできそうなライブラリを見つけました。

Pythonのパッケージ管理ソフト「pip」で簡単にインストールできるみたいなのでとりあえずインストールしてみます。

sudo -H pip install --upgrade youtube-dl

インストールが終わったらシェル上で

youtube-dl [ダウンロードしたい動画のURL]

とするだけでとにかく動画のダウンロードができるようになりました。

ちなみにyoutube-dlという名前ですが、YouTube以外にも様々な動画サイトに対応しており、niconico動画Twitterの動画なんかもダウンロードできます。

youtube-dlが対応しているサイトは以下ページを見てください。

youtube-dl: Supported sites

Pythonで動画をカットする

Pythonで動画をカットするためにMoviePyというライブラリを使う予定でしたが、結論から言うとカット後に再エンコードが必要で処理速度に不満があったので最終的にはffmpegを採用しました。

ffmpegというとエンコードに使われるライブラリのイメージですが、実はカットや音声ファイルの抽出などの簡単な編集もできます。

ちなみにMoviePyだとテキストを入れたりなんかの結構本格的な編集も可能なので、使い方次第では動画の音声を勝手に文字起こししてテロップを自動生成するなんてこともできそうです。

動画編集って臨機応変に色々考えないといけないものですが、MoviePyを使えばある程度自動化できてしまうかもしれません。

ffmpegのインストールはaptコマンドでサクッとしておきます。

sudo apt install ffmpeg

解像度が720p程度になってしまう

youtube-dlをインストールして適当にダウンロードすると最高画質ではなく、HD(1280×720, 720p)程度の画質でダウンロードされてしまいます

デフォルトでは映像と音声が一緒になった状態でダウンロード出来る画質を自動的に選択するので画質が低めに設定されるようです。

楽して切り抜きを作りたいですが、だからといって画質を犠牲にはしたくなかったのでFHD(1920×1080, 1080p)程度でダウンロード出来るようにしてみたいと思います。

解像度はダウンロード時に渡すパラメーター「format」で設定するのですが、デフォルトだとbestになっています。

これは「映像と音声が一緒になった状態でダウンロード出来る中で一番いい画質」という意味になり、YouTubeだと大体の場合は720p程度の画質になります。

画質をより高くするには映像を音声を別にダウンロードしてから結合する必要があるのですが、適当に指定すると映像と音声の形式の相性などの問題でうまくダウンロード出来ないという問題が発生します。

youtube-dlを使いやすくするScript – Qiita

こちらの記事を参考に取得出来る画質の一覧を出力してみるとわかりますが、ダウンロードしたい形式の数字を入れる必要があります。

youtube-dl --list-format <動画のIDもしくはURL>
$ youtube-dl --list-format https://www.youtube.com/watch?v=pFgUluV_00s
[youtube] pFgUluV_00s: Downloading webpage
[info] Available formats for pFgUluV_00s:
format code  extension  resolution note
249          webm       audio only tiny   56k , opus @ 50k (48000Hz), 1.54MiB
250          webm       audio only tiny   73k , opus @ 70k (48000Hz), 2.03MiB
140          m4a        audio only tiny  130k , m4a_dash container, mp4a.40.2@128k (44100Hz), 3.95MiB
251          webm       audio only tiny  144k , opus @160k (48000Hz), 4.01MiB
394          mp4        256x144    144p   87k , av01.0.00M.08, 30fps, video only, 2.38MiB
278          webm       256x144    144p  107k , webm container, vp9, 30fps, video only, 2.85MiB
160          mp4        256x144    144p  130k , avc1.4d400c, 30fps, video only, 3.10MiB
395          mp4        426x240    240p  185k , av01.0.00M.08, 30fps, video only, 5.06MiB
242          webm       426x240    240p  241k , vp9, 30fps, video only, 6.36MiB
133          mp4        426x240    240p  265k , avc1.4d4015, 30fps, video only, 4.62MiB
396          mp4        640x360    360p  327k , av01.0.01M.08, 30fps, video only, 8.84MiB
243          webm       640x360    360p  464k , vp9, 30fps, video only, 11.48MiB
134          mp4        640x360    360p  575k , avc1.4d401e, 30fps, video only, 8.64MiB
397          mp4        854x480    480p  579k , av01.0.04M.08, 30fps, video only, 15.21MiB
244          webm       854x480    480p  781k , vp9, 30fps, video only, 20.23MiB
135          mp4        854x480    480p  791k , avc1.4d401f, 30fps, video only, 12.54MiB
398          mp4        1280x720   720p 1213k , av01.0.05M.08, 30fps, video only, 29.36MiB
136          mp4        1280x720   720p 1426k , avc1.4d401f, 30fps, video only, 21.86MiB
247          webm       1280x720   720p 1554k , vp9, 30fps, video only, 39.99MiB
399          mp4        1920x1080  1080p 2172k , av01.0.08M.08, 30fps, video only, 51.80MiB
248          webm       1920x1080  1080p 2708k , vp9, 30fps, video only, 71.32MiB
137          mp4        1920x1080  1080p 4378k , avc1.640028, 30fps, video only, 74.46MiB
18           mp4        640x360    360p  711k , avc1.42001E, mp4a.40.2@ 96k (44100Hz), 21.67MiB
22           mp4        1280x720   720p  846k , avc1.64001F, mp4a.40.2@192k (44100Hz) (best)

と言った具合に数字が出てくるので、これを組み合わせることで画質と音質の指定が出来ます。

ですが、video onlyaudio onlyと書かれているものは映像のみ・音声のみをダウンロードするための設定です。

さらに楽に切り抜きたいということで作っているので、出力ファイルは手軽に扱えるmp4で出力したい→つまりmp4でダウンロードしたいという前提があるので映像はmp4音声はm4aでダウンロードしたいのです。

上の数字から選ぶと映像は399 mp4 1920x1080 1080p 2172k , av01.0.08M.08, 30fps, video only, 51.80MiBを、音声は140 m4a audio only tiny 130k , m4a_dash container, mp4a.40.2@128k (44100Hz), 3.95MiBを選択したいので、formatには399+140という値を渡せばいいのですが、中にはそのフォーマットをサポートしてない動画もあるので面倒です。

ということで、最終的には以下のようになりました。

ydl_opts = {
    'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
    'outtmpl': 'download/'+'%(id)s.%(ext)s',
    'merge_output_format': 'mp4',
    'add-metadata': True
}

formatにはbestvideo[ext=mp4]+bestaudio[ext=m4a]/bestを指定しました。

bestvideoで一番画質の良いフォーマットを指定していますが、[ext=mp4]をつけることで「mp4の中で一番画質の良いフォーマット」という指定をしています。

同じくbestaudio[ext=m4a]を指定することで「m4aでダウンロードできる中で一番良いフォーマット」を指定しています。

その後ろの/bestは「1つ目の設定が存在しない時はbest(デフォルト)を使用する」という意味です。

/で区切ると先頭から実行して指定したフォーマットがない場合は次のフォーマットを試すという意味になります。

ffmpegで動画をカットする

ということで動画をダウンロードできたのでffmpegでダウンロードした動画を簡単に切り抜いてみようと思います。

ffmpeg -ss [切り抜き開始位置(秒)] -t [開始位置から切り取る秒数] -i [切り抜きたいファイル] -vcodec copy -acodec copy [出力ファイル名] -y

ffmpegを使ってシェルから簡単に切り抜く方法を見つけたのでPython上からシェルを実行してffmpegを実行します。

cmd = 'ffmpeg -ss ' + str(start) + ' -i ' + input_file + ' -t ' + \
    str(end - start) + \
    ' -vcodec copy -acodec copy ' + outputfile + ' -y'
subprocess.check_call(cmd.split())

雑ですが、こんな感じでPythonからシェルコマンドを実行します。

オプションには変数を使うために文字列結合で無理やりつなげています。

もっとスマートな方法があると思いますが、調べるのがめんどいなので思考停止してこんな実装に・・・。

スポンサードリンク

Pythonから簡単にWEB画面を作る

ということでだいたいの処理ができたのですが、この状態ではLinuxのシェルからしか起動できません。

ということで簡単にWEB画面を作ってみます。

Flaskというライブラリを使うことで簡単にWEBサーバーを立ち上げることができるみたいなので、それを利用します。

ただし、Flaskはプロトタイプ作成などに使う開発用ツールのようなので公開する予定のページには使わないようにしましょう。

セキュリティ的にあまり良くないとかどうとか・・・。

ということでできたのがこんな簡単な画面です。

URLと切り抜きたい時間の最初と最後を入れ、ダウンロードのみなんかもできるようにしてみました。

この辺は自分しか使わないのでわかればOKですね!

ソースコード

今回作ったサンプルコードを置いておきますが、正直セキュリティなんかは全く考えていないので、自宅サーバーで動かす程度に留めてください。
何があっても筆者は保証できませんので、自己責任でお願いします。

YouTubeの動画をダウンロードする行為は著作権侵害になることがあります。
法律を遵守して著作権侵害とならない範囲で使ってください。
このコードの実行によって生じた損害について、筆者は保証・補填はできません。

youtube.py

#!/usr/bin/env python3
import youtube_dl
import re
import sys
import os
import time
import subprocess
import lxml
from lxml import etree
from bs4 import BeautifulSoup
from urllib.request import urlopen, urlretrieve
from moviepy.editor import *
from flask import Flask, render_template, request, send_file, make_response, send_from_directory
from pydub import AudioSegment
import urllib.request

app = Flask(__name__)


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/output', methods=['GET', 'POST'])
def output():
    if request.method == 'POST':
        download_only = 0
        force = 0

        if request.form['URL'] == "":
            return render_template('index.html')
        else:
            URL = request.form['URL']

            if request.form.get('download_only') == "download_only":
                download_only = 1

            if request.form.get('force') == "force":
                force = 1

            if request.form['start'] == "" and request.form['end'] == "":
                download_only = 1
            else:
                if ':' in str(request.form['start']):
                    start = str(request.form['start']).split(":")
                    if len(start) == 2:
                        start = int(start[0]) * 60 + int(start[1])
                    elif len(start) == 3:
                        start = int(start[0]) * 3600 + \
                            int(start[1]) * 60 + int(start[2])
                else:
                    start = int(request.form['start'])

                if ":" in request.form['end']:
                    end = str(request.form['end']).split(":")
                    if len(end) == 2:
                        end = int(end[0]) * 60 + int(end[1])
                    elif len(end) == 3:
                        end = int(end[0]) * 3600 + \
                            int(end[1]) * 60 + int(end[2])
                else:
                    end = int(request.form['end'])

            if "https://www.youtube.com/" in URL and "list=" in URL:
                th_list = URL.split("list=")[1]
            else:
                th_list = URL

            if request.form['download_type'] == "mp4":
                ydl_opts = {
                    'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
                    'outtmpl': 'download/'+'%(id)s.%(ext)s',
                    'merge_output_format': 'mp4',
                    'add-metadata': True
                }

            if request.form['download_type'] == "mp3":
                ydl_opts = {
                    'format': 'bestaudio/best',
                    'outtmpl': 'download/' + '%(id)s.%(ext)s',
                    'postprocessors': [
                        {'key': 'FFmpegExtractAudio',
                         'preferredcodec': 'mp3',
                         'preferredquality': '320'},
                        {'key': 'FFmpegMetadata'},
                    ],
                }

            ydl = youtube_dl.YoutubeDL(ydl_opts)
            info_dict = ydl.extract_info(th_list, download=False)

            # print(info_dict)

            print(info_dict['id'])
            print(info_dict['channel_id'])
            print(info_dict['title'])
            print(info_dict['thumbnail'])

            if download_only == 1:
                path = info_dict['id'] + "." + \
                    str(request.form['download_type'])
                download_filename = info_dict['title'] + "_" + info_dict['id'] + "." + \
                    str(request.form['download_type'])

            if download_only == 0:
                path = info_dict['id'] + "_" + \
                    str(start) + "_" + str(end) + "." + \
                    str(request.form['download_type'])
                download_filename = info_dict['title'] + "_" +  \
                    info_dict['id'] + "_" + \
                    str(start) + "_" + str(end) + "." + \
                    str(request.form['download_type'])

            if os.path.isfile("download/" + path) and force == 0:
                return send_from_directory(os.getcwd() + "/download", path,
                                           as_attachment=True, attachment_filename=download_filename)
            else:
                info_dict = ydl.extract_info(th_list, download=True)

                urllib.request.urlretrieve(
                    info_dict['thumbnail'], "download/" + info_dict['id'] + ".jpg")

                input_file = "download/" + info_dict['id'] + "." + \
                    str(request.form['download_type'])

                if download_only == 1:
                    filename = info_dict['id'] + "." + \
                        str(request.form['download_type'])
                    download_filename = info_dict['title'] + \
                        "_" + info_dict['id'] + "." + \
                        str(request.form['download_type'])

                elif download_only == 0 and request.form['download_type'] == "mp4":
                    filename = info_dict['id'] + "_" + \
                        str(start) + "_" + str(end) + ".mp4"
                    download_filename = info_dict['title'] + "_" + \
                        info_dict['id'] + "_" + \
                        str(start) + "_" + str(end) + ".mp4"  # 編集後のファイル保存先とパス

                    outputfile = "download/" + \
                        info_dict['id'] + "_" + \
                        str(start) + "_" + str(end) + ".mp4"

                    cmd = 'ffmpeg -ss ' + str(start) + ' -i ' + input_file + ' -t ' + \
                        str(end - start) + \
                        ' -vcodec copy -acodec copy ' + outputfile + ' -y'
                    subprocess.check_call(cmd.split())

                elif download_only == 0 and request.form['download_type'] == "mp3":
                    filename = info_dict['id'] + "_" + \
                        str(start) + "_" + str(end) + ".mp3"
                    download_filename = info_dict['title'] + "_" +  \
                        info_dict['id'] + "_" + \
                        str(start) + "_" + str(end) + ".mp3"
                    outputfile = "download/" + \
                        info_dict['id'] + "_" + \
                        str(start) + "_" + str(end) + ".mp3"

                    sound = AudioSegment.from_file(input_file, format="mp3")
                    sound1 = sound[start*1000:end*1000]
                    sound1.export(outputfile, format="mp3")

                return send_from_directory(os.getcwd() + "/download", filename,
                                           as_attachment=True, attachment_filename=download_filename)
    else:
        return render_template('index.html')


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8080, debug=True)

templates/index.html

{% extends "layout.html" %}
{% block content %}
<form action="/output" method="post">
    <div>
        <label>URL</label>
        <input type="text" id="URL" name="URL">
    </div>
    <div>
        <label>時間(秒数)</label>
        開始<input type="tel" id="start" name="start"><br>
        終了<input type="tel" id="end" name="end"><br>
        ダウンロードのみ<input type="checkbox" id="download_only" name="download_only" value="download_only">
        force<input type="checkbox" id="force" name="force" value="force">
        <select name="download_type">
            <option value="mp4" selected>mp4</option>
            <option value="mp3">mp3</option>
        </select>

    </div>
    <div>
        <button>実行開始!</button>
    </div>
</form>
{% endblock %}

templates/layout.html

<!doctype html>
<html>

<head>
    <title>youtube切り抜きテスト</title>
</head>

<body>
    {% block content %}
    <!-- ここにメインコンテンツを書く -->
    {% endblock %}
</body>

</html>

まとめ

思ったよりもハードルが低かった

作る前は結構時間かかるだろうなぁ・・・と思いながら重い腰を上げた感じでしたが、実際作ってみると1週間ぐらいで大体の機能は実装できました。

とりあえず動くようになるまでに掛かった時間は3日ぐらいなので、スピード感のある開発だったと思います。

世の中の偉い人たちがライブラリとかを作ってくれていることが多いので、先人の知恵を継ぎ接ぎしていけば意外と簡単に作れてしまいます。

それこそGithubとかでそれっぽいサンプルコードを探してパクって拝借すれば意外と簡単に作れてしまいます。

課題は高速化

今回のツールは、切り抜きたい動画をダウンロードしてから切り抜き処理を行うので、どうしても時間がかかってしまいます。

切り抜き処理自体は数秒程度で終わるほど高速なので、動画全体をダウンロードしなくても済むようにできれば高速化できそうですが・・・。

あとはエンコードしないで動画を出力するという特性からか、指定時間より動画が短くなってしまったり、動画の最後の方のデータが壊れてしまって映像だけ止まってしまうことがあったりと不具合はちょいちょいあります。

ですが、意外と簡単に作れたので満足しました。

完全に自己満足でしたが、意外とフォーマットの指定のあたりはネットにも情報がなかったので記事にしました。

誰かの役に立てれば幸いです。

コメント

タイトルとURLをコピーしました