VideoLoopProcessor: 動画とオーディオを組み合わせる魔法のツール

## はじめに

皆さん、こんにちは！今日は、「VideoLoopProcessor」という面白いプログラムについてお話しします。このプログラムは、動画をオーディオファイルの長さに合わせてループ処理する素晴らしいツールです。簡単に言えば、短い動画を音楽の長さに合わせて繰り返し再生させる魔法のようなプログラムなんです！


## デモ動画

<blockquote class="twitter-tweet" data-media-max-width="800"><p lang="ja" dir="ltr">KLINGなどで生成した短い動画をループさせるライブラリができそう！！！<br>ちなみに順再生と逆再生の倍率をランダムにしています。。。後々は音楽に合わせて変えれたらいいかも！！！ <a href="https://t.co/c4Mm9LwUO7">https://t.co/c4Mm9LwUO7</a> <a href="https://t.co/WKW8V3wCho">pic.twitter.com/WKW8V3wCho</a></p>&mdash; Maki@Sunwood AI Labs. (@hAru_mAki_ch) <a href="https://twitter.com/hAru_mAki_ch/status/1816392607893000689?ref_src=twsrc%5Etfw">July 25, 2024</a></blockquote> 

## VideoLoopProcessorの全体像

まずは、このプログラムが何をするのか、全体像を見てみましょう。

```mermaid
%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#024959',
    'primaryTextColor': '#F2C12E',
    'primaryBorderColor': '#024959',
    'lineColor': '#A1A2A6',
    'secondaryColor': '#F2AE30',
    'tertiaryColor': '#593E25',
    'textColor': '#A1A2A6',
    'fontSize': '20px'
  }
} }%%
graph TD
    A[入力: 短い動画] --> B[VideoLoopProcessor]
    C[入力: 音楽ファイル] --> B
    B --> D[出力: 音楽の長さに合わせた動画]
    B -->|1. 動画を逆再生| E[逆再生動画作成]
    B -->|2. 速度変更| F[動画速度をランダムに変更]
    B -->|3. 動画結合| G[複数の動画を結合]
    B -->|4. 音楽と合成| H[最終的な動画作成]
```

この図を見ると、VideoLoopProcessorが行う主な処理が分かりますね。短い動画と音楽ファイルを入力として受け取り、最終的に音楽の長さに合わせた動画を作成します。その過程で、動画の逆再生、速度変更、結合、音楽との合成といった様々な処理を行っているんです。

## プログラムの詳細な流れ

では、このプログラムがどのように動作するのか、もう少し詳しく見ていきましょう。

### 1. 初期設定

まず、プログラムを使うために必要な情報を設定します。

```python
input_file = "cat_is_playing_dj.mp4"
output_file = "output.mp4"
audio_file = "input_tracks_enka/o_青い夜の響き.mp3"

processor = VideoLoopProcessor(input_file, output_file, audio_file)
```

ここでは、入力する動画ファイル（猫がDJをしている動画らしいですね！）、出力するファイル名、そして使用する音楽ファイル（演歌のようです）を指定しています。

### 2. 動画処理の開始

次に、`process`メソッドを呼び出して実際の処理を開始します。

```python
processor.process()
```

この一行で魔法のような処理が始まります！では、その中身を見ていきましょう。

### 3. 逆再生動画の作成

まず、入力された動画を逆再生した新しい動画を作成します。

```mermaid
%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#024959',
    'primaryTextColor': '#F2C12E',
    'primaryBorderColor': '#024959',
    'lineColor': '#A1A2A6',
    'secondaryColor': '#F2AE30',
    'tertiaryColor': '#593E25',
    'textColor': '#A1A2A6',
    'fontSize': '20px'
  }
} }%%
sequenceDiagram
    participant O as 元の動画
    participant F as FFmpeg
    participant R as 逆再生動画
    O->>F: 動画データ送信
    F->>F: 逆再生処理
    F->>R: 逆再生動画作成
```

このステップで、元の動画を逆再生した新しい動画ファイルができあがります。猫が後ろ向きにDJをしているような面白い動画になっているでしょうね！

### 4. 動画のループ処理

ここからが本当の魔法です。音楽の長さに合わせて、元の動画と逆再生動画を交互に使いながら、速度を変えてループさせていきます。

```mermaid
%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#024959',
    'primaryTextColor': '#F2C12E',
    'primaryBorderColor': '#024959',
    'lineColor': '#A1A2A6',
    'secondaryColor': '#F2AE30',
    'tertiaryColor': '#593E25',
    'textColor': '#A1A2A6',
    'fontSize': '20px'
  }
} }%%
graph TD
    A[音楽の長さを取得] --> B{十分な長さか?}
    B -->|No| C[動画選択:元動画か逆再生]
    C --> D[速度をランダムに決定]
    D --> E[速度変更した動画を作成]
    E --> F[合計時間を更新]
    F --> B
    B -->|Yes| G[ループ終了]
```

この処理を繰り返すことで、音楽の長さに合わせた面白い動画ができあがっていくんです。速度がランダムに変わるので、見ていて飽きない動画になりますよ！

### 5. 動画の結合

ループ処理で作成した複数の動画を一つにまとめます。

```mermaid
%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#024959',
    'primaryTextColor': '#F2C12E',
    'primaryBorderColor': '#024959',
    'lineColor': '#A1A2A6',
    'secondaryColor': '#F2AE30',
    'tertiaryColor': '#593E25',
    'textColor': '#A1A2A6',
    'fontSize': '20px'
  }
} }%%
sequenceDiagram
    participant V as 複数の動画
    participant F as FFmpeg
    participant C as 結合動画
    V->>F: 動画リスト送信
    F->>F: 動画結合処理
    F->>C: 一つの動画として結合
```

これで、音楽の長さに合わせた一本の動画ができあがりました！

### 6. 音楽との合成

最後に、結合した動画と元の音楽ファイルを合成します。

```mermaid
%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#024959',
    'primaryTextColor': '#F2C12E',
    'primaryBorderColor': '#024959',
    'lineColor': '#A1A2A6',
    'secondaryColor': '#F2AE30',
    'tertiaryColor': '#593E25',
    'textColor': '#A1A2A6',
    'fontSize': '20px'
  }
} }%%
sequenceDiagram
    participant V as 結合動画
    participant A as 音楽ファイル
    participant F as FFmpeg
    participant O as 最終出力動画
    V->>F: 動画データ送信
    A->>F: 音楽データ送信
    F->>F: 動画と音楽の合成処理
    F->>O: 最終的な動画を作成
```

これで完成です！猫のDJ動画が、演歌に合わせてユニークなリズムで動く面白い動画ができあがりました。


## 全体コード

```python
import subprocess
import sys
import random
import os
import shutil
from loguru import logger
from typing import List

from art import *
class VideoLoopProcessor:
    """
    動画をオーディオファイルの長さに合わせてループ処理するクラス。
    """

    def __init__(
        self,
        input_file: str,
        output_file: str,
        audio_file: str,
        output_dir: str = "output",
        temp_file_prefix: str = "temp_",
        speed_min: float = 0.7,
        speed_max: float = 1.3
    ):
        self.input_file = input_file
        self.output_file = output_file
        self.audio_file = audio_file
        self.output_dir = os.path.join(output_dir, os.path.splitext(os.path.basename(self.input_file))[0])
        self.temp_file_prefix = temp_file_prefix
        self.speed_min = speed_min
        self.speed_max = speed_max

        self.reversed_file = f"{self.temp_file_prefix}reversed.mp4"
        self.temp_combined_file = f"{self.temp_file_prefix}combined.mp4"
        self.filelist_name = "filelist.txt"

        os.makedirs(self.output_dir, exist_ok=True)
        self._copy_files_to_output_dir()

    def _copy_files_to_output_dir(self):
        """入力ファイルとオーディオファイルを処理ディレクトリにコピーする"""
        logger.info(f"入力ファイル '{self.input_file}' を '{self.output_dir}' にコピーしています...")
        shutil.copy2(self.input_file, self.output_dir)
        logger.info(f"入力ファイル '{self.audio_file}' を '{self.output_dir}' にコピーしています...")
        shutil.copy2(self.audio_file, self.output_dir)

    def _get_audio_duration(self, audio_file: str) -> float:
        """指定されたオーディオファイルの長さを秒単位で取得する"""
        logger.info(f"オーディオファイル '{audio_file}' の長さを取得しています...")
        result = subprocess.run(
            [
                "ffprobe",
                "-v", "error",
                "-show_entries", "format=duration",
                "-of", "default=noprint_wrappers=1:nokey=1",
                os.path.basename(audio_file)
            ],
            capture_output=True,
            text=True,
            cwd=self.output_dir  
        )
        duration = float(result.stdout)
        logger.info(f"オーディオの長さ: {duration:.2f}秒")
        return duration

    def _concat_videos(self, file_list: List[str], output_file_path: str):
        """ファイルリスト内のビデオファイルを連結する"""
        filelist_path = os.path.join(self.output_dir, self.filelist_name)
        with open(filelist_path, "w") as f:
            for file_path in file_list:
                f.write(f"file '{file_path}'\n")
        subprocess.run(
            ["ffmpeg", "-f", "concat", "-safe", "0", "-i", self.filelist_name, "-c", "copy", output_file_path],
            check=True,
            cwd=self.output_dir,
        )
        logger.info(f"出力ファイル '{output_file_path}' を作成しました")

    def process(self):
        """動画を処理するメインのメソッド"""
        tprint(">>  VideoLoopProcessor", font="rnd-large")
        logger.info("動画を逆再生しています...")
        subprocess.run(["ffmpeg", "-i", self.input_file, "-vf", "reverse", self.reversed_file], cwd=self.output_dir)

        videos = [os.path.basename(self.input_file), self.reversed_file]
        total_duration = 0
        i = 0

        while total_duration < self._get_audio_duration(self.audio_file):
            video = videos[i % 2]
            i += 1

            speed = random.uniform(self.speed_min, self.speed_max)
            temp_file = f"{self.temp_file_prefix}{int(total_duration * 1000):06d}.mp4"

            logger.info(f"動画 '{video}' を速度 {speed:.2f}x で処理しています...")
            subprocess.run(
                [
                    "ffmpeg",
                    "-i", video,
                    "-filter:v", f"setpts={1 / speed}*PTS",
                    temp_file
                ],
                cwd=self.output_dir
            )

            videos.append(temp_file)
            total_duration += self._get_audio_duration(os.path.join(self.output_dir, temp_file))

        logger.info("動画を結合しています...")
        self._concat_videos(videos, self.temp_combined_file)

        logger.info("MP3と結合しています...")
        subprocess.run(
            [
                "ffmpeg",
                "-i", self.temp_combined_file,
                "-i", os.path.basename(self.audio_file),
                "-map", "0:v",
                "-map", "1:a",
                "-c:v", "copy",
                "-shortest",
                self.output_file
            ],
            cwd=self.output_dir
        )

        logger.info(f"最終出力ファイル '{os.path.join(self.output_dir, self.output_file)}' を作成しました")


if __name__ == "__main__":
    input_file = "cat_is_playing_dj.mp4"
    output_file = "output.mp4"
    audio_file = "input_tracks_enka/o_青い夜の響き.mp3" 

    processor = VideoLoopProcessor(input_file, output_file, audio_file)
    processor.process()

```

## まとめ

VideoLoopProcessorは、短い動画を音楽に合わせて繰り返し再生する素晴らしいツールです。動画を逆再生したり、速度を変えたりすることで、単純な繰り返しではない面白い動画を作り出します。

このプログラムを使えば、短い面白い動画を長い音楽に合わせて延長したり、音楽に合わせてリズミカルに動く動画を作ったりすることができます。YouTubeやSNSで使える面白いコンテンツを作るのに最適なツールと言えるでしょう。

ぜひ、あなたも試してみてください。きっと素晴らしい作品ができあがるはずです！

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>