Python

動画の音声が聞こえる区間だけ口パクする立ち絵を作成する

Posted on 2022年3月10日 (Last modified on 2023年1月14日) 2 min read  • 396 words
動画の音声が聞こえる区間だけ口パクする立ち絵を作成する

概要

mp4から音声を取り出し、音声が一定レベル以上の区間で口パクして、たまに瞬きもしてくれる立ち絵の動画を作成したいです。立ち絵は春日部つむぎさんを使用。

TOP2 | 春日部つむぎ公式HP

ちなみに、以下のPythonコードを元に初めて作った口パクは以下の動画です。

立ち絵に使用する画像

psdファイルをPythonで読み込みレイヤーの表示・非表示を切り替える』で作成した画像群の中から、口と目が開いている・閉じている画像を使用します。 コードで使用している立ち絵は以下のサイトのものであり、動画(gif)形式以外の実行結果はこの記事やコードには載せていません。

RULE | 春日部つむぎ公式HP

開発環境

ffmpegをインストールする必要があります。

brew install ffmpeg

また、以下のコードはpython3.6で実行してます。 コードは後ほどgithubに上げる予定です。

mp4からの画像の抽出

OBSで録画したりAdobe premire Proで動画を作成した動画を使用します。

音声の抽出

ffmpeg Documentationを確認しつつ、mp4のファイルから音声(wavファイル)を抽出します。コマンドラインでffmpegを実行することで変換できます。

  • -i url (input) - input file url (入力ファイルの指定)

  • -y (global) - Overwrite output files without asking. (-y指定なら既にファイルがある場合上書き)

import subprocess

mp4_path = './mp4/DTW.001.mp4'
wav_path = './mp4/output.wav'
subprocess.call(f'ffmpeg -i {mp4_path} {wav_path}', shell=True)

音声区間を抽出

inaSpeechSegmenterの実装を使って音声の区間を抽出してみます。 warnings.filterwarnings("ignore", category=DeprecationWarning) としているのは、inaSpeechSegmenter内部でnumpyのDeprecationWarningが多く出力されていたためです。seg2csvで検出した区間をcsvに保存しています。

from inaSpeechSegmenter import Segmenter
from inaSpeechSegmenter.export_funcs import seg2csv
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

model = Segmenter(vad_engine='smn', detect_gender=True)
segmentations = model('../mp4/DTW.001.wav')
seg2csv(segmentations, '../mp4/DTW.001.csv')
print(segmentations)
[('music', 0.0, 2.94), ('female', 2.94, 9.74), ('noise', 9.74, 11.76), ('female', 11.76, 227.70000000000002), ('music', 227.70000000000002, 229.24), ('female', 229.24, 375.12), ('noEnergy', 375.12, 375.6), ('female', 375.6, 376.96)]

数秒無言の区間がないと、連続してしゃべっていると判定されるみたいです。保存したデータはtab区切りになっています。

import pandas as pd
segs = pd.read_csv('../mp4/DTW.001.csv', sep='\t')
segs.head()

口パク・瞬きの追加

作成した音声の区間だけ口パクが入るような動画を作成します。

静止画から動画を作成

複数の立ち絵から動画を出力してみます。 これができたらあとは音声の区間だけ口パクして瞬きを追加するだけです。高いfpsは不要なので fps=5 と指定。

import cv2

# 動画に使用する立ち絵をロード
image_size = (200, 400)
image_1 = cv2.imread('output/口=*へっ_目=*ジト目_口=*悲しい.png')
image_2 = cv2.imread('output/口=*ω_目=*なごみ_口=*困る.png')
image_3 = cv2.imread('output/口=*ω_目=*なごみ_口=*普通.png')
image_4 = cv2.imread('output/口=*ω_目=*ハイライト無_口=*普通.png')
image_5 = cv2.imread('output/口=*ω_目=*なごみ_口=*普通.png')

# 出力する動画に画像サイズを合わせる
images = [image_1, image_2, image_3, image_4, image_5]
images = [cv2.resize(img, dsize=image_size) for img in images]

# 毎フレームの画像を描画
fps = 5
fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
video = cv2.VideoWriter('output_video/口パク.mp4',fourcc, fps, image_size)

if not video.isOpened():
    print("can't be opened")
    sys.exit()

# 20秒の動画を作成
for i in range(0, 100):
    img = images[i%5]
    video.write(img)

video.release()

生成された動画もきっかり20秒であることが確認できました。

口パクの作成

inaSpeechSegmenterで抽出した音声の区間だけ口パクを挟みます。「静止画から動画を作成」のコードを少しいじるだけです。

import cv2
import numpy as np
import pandas as pd


# 動画に使用する立ち絵をロード
image_size = (100, 200)
image_1 = cv2.imread('output_image/口=*ω わ_目=*普通(横目)_眉=*普通.png')
image_2 = cv2.imread('output_image/口=*3_目=*普通(横目)_眉=*普通.png')

# 出力する動画に画像サイズを合わせる
images = [image_1, image_2]
images = [cv2.resize(img, dsize=image_size) for img in images]

# 毎フレームの画像を描画
fps = 10.0
fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
video = cv2.VideoWriter('output_video/口パク_会話に合わせる.mp4',fourcc, fps, image_size)

if not video.isOpened():
    print("can't be opened")
    sys.exit()

# 20秒の動画を作成
segs = pd.read_csv('mp4/DTW.001.csv', sep='\t')
voice_starts, voice_ends = segs['start'], segs['stop']
voice_noenergy = segs["labels"] == "noEnergy"
voice_index = 0  # 現在の発話区間のインデックス
is_during_speech = False  # 発話中のフラグ
lip_sync_spacing_time = 1  # 口パクのフレーム間隔

for i in range(0, 100):
    current_sec = i * (1.0/fps)

    # 音声の区間
    if voice_starts[voice_index] <= current_sec <= voice_ends[voice_index]:
        # 発話中
        if is_during_speech and lip_sync_spacing_time > 0:
            lip_sync_spacing_time -= 1
            img = images[1]
        elif is_during_speech and lip_sync_spacing_time == 0:
            lip_sync_spacing_time = np.random.randint(2, 4)
            img = images[0]
        else:
            is_during_speech = True
            lip_sync_spacing_time = np.random.randint(2, 4)
            img = images[0]
    elif current_sec < voice_starts[voice_index]:
        # 発話前
        img = images[0]
    elif voice_ends[voice_index] < current_sec:
        # 発話後
        voice_index += 1  # 次のインデックスに進める
        if is_during_speech:
            is_during_speech = False
            img = images[1]
        else:
            # 発話区間が短すぎる場合
            img = images[1]

    if is_no_energy:
        # 無音区間では強制的に口を閉じる
        img = get_close_mouse_img()

    video.write(img)

video.release()
実行結果

感想

ランダムに口パクや瞬きをしているだけだと、なんというか生きている感じがしなかったです。 この辺りもう少し修正してから動画に使用してみようと思いました。また、こんな素晴らしいテキスト読み上げソフトウェアを開発して頂いた方々に改めて感謝します。

¯\_(ツ)_/¯

コーヒーと蝶が好きです。