Python

Pythonで動画の無音部分をカットして結合。動画編集の自動化に!soundfile、matplotlib

Pythonで動画の無音部分をカットして結合。動画編集の自動化に!soundfile、matplotlib Python
この記事は約12分で読めます。
piyo
piyo
今日の運勢

Pythonで動画の無音部分手順

今回は撮影された動画の無音部分をカットして動画編集をしやすくするデータを作成します。

  1. 動画から音データを取り出す。
  2. 音声ファイルをグラフ化させて、削除部分を検討
  3. 自然につながるように間を調整
  4. 動画を分割して結合させる

制作時の設定

細かい秒数などは、個人の好みで調整してください。

無音の定義

  • thres = 0.05: 閾値(thres)は、音声の振。幅振幅がこの閾値よりも大きい場合、その部分は有音(音がある)と見なしています。
  • min_silence_duration = 0.5: min_silence_durationは、サイレンスと見なす最小の無音期間の長さを定義し、音声データ内でこの期間よりも短い無音部分は無視されます。
  • 途中、自然に繋げるための無音部分から0.2秒間をつける作業しています。

無音の繋ぎを自然にする調整

  • 途中、自然に繋げるための無音部分から0.2秒間をつける作業しています。

ファイル名フォルダ名

  • test.mp4が元動画。test.pyは、test.wavでを作るコードファイル。test1.pyは、無音部分を削除した動画を作るコードファイルにしています。
  • 新規フォルダの中に、途中データは、「数字_keep.mp4」、完成データは、concatenated.mp4が作成されるようにしています。

Pythonのバージョン

Python3.12.1のバージョンを使用しています。

Dockを立ち上げる

command+「スペースキー」で検索窓が出てくる>「ターミナル」と記入>

「自分のPC名〜%」の後ろに記入していきます。

(例)gotoaya@AGMacBook-Air ~ % ←ここの後ろに記入

ffmpegのインストール

ffmpegを入れた記憶のない方はコチラを記入してインストール
(下記は、Homebrewの方用の呪文です)

brew install ffmpeg

ずらっと色々書かれていきます。少し時間がかかるので待ちましょう。

動画から音声データを取得

このスクリプトを実行すると、test.mp4 からオーディオが抽出され、指定されたパラメータで test.wav として保存されます。

.wavデータを作成する

import subprocess

# ffmpegコマンドの引数をリストで指定
ffmpeg_command = [
    'ffmpeg',
    '-i', 'test.mp4',
    '-ac', '1',
    '-ar', '44100',
    '-acodec', 'pcm_s16le',
    'test.wav'
]

# subprocessモジュールを使用してffmpegコマンドを実行
subprocess.run(ffmpeg_command)

Pythonの拡張子で保存

Pythonの拡張子(.py)で保存して画像と同じフォルダに入れる。

一つのフォルダに動画と.pyコードを格納

今回は「test.py」という名前のフォルダに格納しました。

Dockを立ち上げる

command+「スペースキー」で検索窓が出てくる>「ターミナル」と記入>

「自分のPC名〜%」の後ろに記入していきます。

(例)gotoaya@AGMacBook-Air ~ % ←ここの後ろに記入

読み込み場所を指定する

cd␣全部格納した、ディレクトリーを記入

cd ディレクトリーを記入

>Enter

piyo
piyo

cdの後ろにスペースを入れないとダメピヨ!

※フォルダーのディレクトリーが分からない時は、フォルダーをターミナルにドラックすると表示されるよ。

「Python ファイル名」を記入して「Enter」

Python␣読み込むファイル名

Python 読み込むファイル名

>Enter

音声ファイルの完成

音声ファイルの読み書きを行うためのモジュールsoundfileインストール

pip install soundfile

soundfile モジュールは、音声ファイルを操作するためのパッケージであり、主に読み込みと書き込みを行います。 NumPy 配列と互換性があり、多くの音声形式(wav、flac、ogg など)に対応しています。

データの可視化やグラフの描画などに使われるライブラリmatplotlib をインストール

pip install matplotlib

matplotlib は、グラフを描画するための広く使われるライブラリです。様々な種類のプロットや図を作成するためのツールセットを提供しており、科学的なデータ可視化やデータ解析に広く利用されています。

動画の無音部分を削除するコード

# 必要なライブラリをインポート
import soundfile as sf
import os
import time
import numpy as np
from matplotlib import pyplot as plt
import subprocess

# 入力音声ファイルのパス
src_file = os.path.join("test.wav")

# 音声ファイルの読み込み
data, samplerate = sf.read(src_file)

# 時間軸を作成して音声データを可視化
t = np.arange(0, len(data)) / samplerate
plt.figure(figsize=(18, 6))
plt.plot(t, data)
plt.show()

# サイレンスを検出するための閾値と最小サイレンスの長さを定義
thres = 0.05
amp = np.abs(data)
b = amp > thres
min_silence_duration = 0.5

# サイレンスの範囲を保存するリスト
silences = []
prev = 0
entered = 0

# サイレンスを検出して範囲を保存
for i, v in enumerate(b):
    if prev == 1 and v == 0:  # サイレンスに入る
        entered = i
    if prev == 0 and v == 1:  # サイレンスから出る
        duration = (i - entered) / samplerate
        if duration > min_silence_duration:
            silences.append({"from": entered, "to": i, "suffix": "cut"})
            entered = 0
    prev = v

# 最後に残ったサイレンスを追加
if entered > 0 and entered < len(b):
    silences.append({"from": entered, "to": len(b), "suffix": "cut"})

# サイレンスの間の音声を保持するための最小の長さ
min_keep_duration = 0.2
cut_blocks = []
blocks = silences

# 連続するサイレンスを結合して音声を抽出
while 1:
    if len(blocks) == 1:
        cut_blocks.append(blocks[0])
        break

    block = blocks[0]
    next_blocks = [block]

    for i, b in enumerate(blocks):
        if i == 0:
            continue
        interval = (b["from"] - block["to"]) / samplerate
        if interval < min_keep_duration:
            block["to"] = b["to"]
            next_blocks.append(b)

    cut_blocks.append(block)
    blocks = list(filter(lambda b: b not in next_blocks, blocks))

# サイレンス以外の音声を保持するためのブロックを作成
keep_blocks = []

for i, block in enumerate(cut_blocks):
    if i == 0 and block["from"] > 0:
        keep_blocks.append({"from": 0, "to": block["from"], "suffix": "keep"})
    if i > 0:
        prev = cut_blocks[i - 1]
        keep_blocks.append({"from": prev["to"], "to": block["from"], "suffix": "keep"})
    if i == len(cut_blocks) - 1 and block["to"] < len(data):
        keep_blocks.append({"from": block["to"], "to": len(data), "suffix": "keep"})

# 出力動画ファイルのパス
mov_file = os.path.join("test.mp4")

# 出力ディレクトリの作成
out_dir = os.path.join("{}".format(int(time.time())))
os.mkdir(out_dir)

# 余白を追加した動画ファイルのパスをファイルに書き込む
concat_file = os.path.join("concat.txt")
with open(concat_file, 'w') as f:
    for i, block in enumerate(keep_blocks):
        fr = block["from"] / samplerate
        to = block["to"] / samplerate
        duration = to - fr
        out_path = os.path.join(out_dir, "{:2d}_{}.mp4".format(i, block["suffix"]))

        # 余白を追加した動画ファイルを生成
        subprocess.run(['ffmpeg', '-ss', str(fr), '-i', mov_file, '-t', str(duration), '-c', 'copy', out_path])

        # 生成した動画ファイルのパスをconcat.txtに書き込む
        f.write("file '{}'\n".format(out_path))

# 前後に余白を加えた動画の結合
concatenated_file = os.path.join(out_dir, "concatenated.mp4")
subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', concat_file, '-c', 'copy', concatenated_file])

波形データが登場

波形データを消すと、ターミナルの書き込みが激しく動いて、データが作成されます。

完成

ファイル名「1705504082」(数字は変わります)を開くと>

データと完成データが格納されています。

解説

サイレンスを検出するための閾値(threshold)の設定

# サイレンスを検出するための閾値(threshold)の設定
thres = 0.05

# 音声データの振幅の絶対値を取得
amp = np.abs(data)

# 閾値を超える振幅を持つ部分をTrue、それ以外をFalseとする二値の配列を生成
b = amp > thres

# 最小のサイレンスの長さの定義(単位は秒)
min_silence_duration = 0.5
  1. thres = 0.05: 閾値(thres)は、音声の振幅がこの値を超えるかどうかを判定する基準です。振幅がこの閾値よりも大きい場合、その部分は有音(音がある)と見なされます。閾値の値は実際の音声データに依存し、適切な値を選ぶ必要があります。
  2. amp = np.abs(data): ampは音声データの振幅の絶対値を示します。音声データは通常、正負の振動を持つため、絶対値を取得して振幅のみを考慮します。
  3. b = amp > thres: bは、振幅が閾値を超えるかどうかを示す真偽値の配列です。Trueは有音(音がある)、Falseは無音(サイレンス)を示します。
  4. min_silence_duration = 0.5: min_silence_durationは、サイレンスと見なす最小の無音期間の長さを定義します。これは秒単位で表され、音声データ内でこの期間よりも短い無音部分は無視されます。

これらの手順によって、音声データが閾値よりも低い振幅を持ち、かつ一定の時間以上続く部分がサイレンスとして検出されます。これは後続の処理で使用され、サイレンス部分を検出してそれを切り取るために利用されます。

サイレンスの間の音声を保持するための最小の長さ

# サイレンスの間の音声を保持するための最小の長さの定義(単位は秒)
min_keep_duration = 0.1

# cut_blocksというリストの初期化
cut_blocks = []

# 初期のサイレンスのリスト(silences)をblocksにコピー
blocks = silences
  1. min_keep_duration = 0.1: min_keep_durationは、サイレンスと見なされた部分から音声を保持するための最小の長さを定義します。つまり、この値よりも短い音声の断片は無視され、サイレンスとして扱われます。単位は秒です。
  2. cut_blocks = []: cut_blocksは、最終的にサイレンス以外の音声部分を保持するためのリストです。このリストには、最小の音声保持期間を超えるサイレンス以外の連続した音声部分が格納されます。
  3. blocks = silences: blocksは初期状態では silences リストのコピーです。silences リストにはサイレンスの範囲(開始時刻、終了時刻)が含まれています。このリストが後の処理で変更されても、元のサイレンス情報は blocks に残っています。

これらの変数と定数は、サイレンスの部分を切り取って得られた情報をもとに、最終的な音声データを保持するためのブロックを作成するために使用されます。次のステップでは、blocks リストを処理して、最終的な音声データを cut_blocks に取りまとめていきます。

コメント

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