5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Fletでチャプター検出できる動画編集アプリを作りました

Posted at

作ったもの

動画内の黒/白のフェードイン/アウトからチャプターを検出して、好きなチャプターだけを選んで結合できるアプリ「ChapterTrimmer」を作りました。

demo

こちらからダウンロードできます(現時点ではWindowsのみ)。

※自分で使うために勢いで作ったので、正常系の簡単なテストしかやってません。ちゃんとしたテストやリファクタリングはあとでやります(フラグ)。

なぜ作ったか

黒や白のフェードイン/アウトで区切られた動画を観ることってよくありますよね。僕はあります。そのなかで必要なチャプターだけ抽出できたらいいなーと思って調べてみたんですが、そのようなことができるアプリが見当たらなかったので作りました。

なぜFletで作ったか

今回はFletという、フロントエンド言語を書かずにPythonだけでFlutterアプリを作れるフレームワークを使いました。選定の理由はこちらです。

  • GB単位の動画ファイルを扱う想定なので、サーバーへのアップロード/ダウンロードが必要なWEBアプリよりもデスクトップアプリがいい
  • デスクトップアプリを作るにはいろんなやり方があるが、Fletが一番ちょうど良さそう
    • Electron:今回のような小規模なアプリには too much 感がある
    • Flutter:一からDartを触るほどのモチベはない
    • Tauri:一からRustを(以下同文)
    • Flet:現状だとE2Eテストをする方法が(PyAutoGUIくらいしか)なかったり開発途上感はあるが、Pythonは使い慣れてるしTkinterやKivyよりいい感じにUIが作れそう

ざっくり説明

動作の流れは以下の通りです。順にざっくり説明していきます。

  • 動画ファイルを読み込む
  • 動画のチャプターを検出する
  • 各チャプターの動画を表示する
  • 選択されたチャプターを結合して保存する

なお、ソースコードはGitHubにあるので詳しくはそちらを見てください。

動画ファイルを読み込む

Fletでファイルを読み込むには以下のように書きます。

def main(page: ft.Page):
    def load_video(e: ft.FilePickerResultEvent):
        picked_video = ft.VideoMedia(e.files[0].path)

        # 何かしらの処理
        # たとえば動画を再生するならこう書く
        video = ft.Video(
                    aspect_ratio=16 / 9,
                    autoplay=False,
                    filter_quality=ft.FilterQuality.HIGH,
                    playlist=[picked_video],
                    playlist_mode=ft.PlaylistMode.NONE,
                )
        page.add(video)
        page.update()

    file_picker = ft.FilePicker(on_result=load_video)
    page.overlay.append(file_picker)

    file_pick_button = ft.Container(
        ft.ElevatedButton(
            "Pick Video File",
            icon=ft.icons.VIDEO_FILE_OUTLINED,
            on_click=lambda _: file_picker.pick_files(allow_multiple=False, file_type=ft.FilePickerFileType.VIDEO),
        ),
    )
    page.add(file_pick_button)
    page.update()


ft.app(target=main)

Fletは公式ドキュメントが充実しているので、それを参考にすればだいたいの処理は書けると思います。

動画のチャプターを検出する

チャプター検出にはPySceneDetectを使いました。

PySceneDetectには以下の3つのDetectorが搭載されています。

  • Content-Aware Detector
    • 動画内のフレームがパッと切り替わったタイミングを検出できる
  • Adaptive Content Detector
    • Content-Aware Detectorと同様にパッと切り替わったのを検出できるが、誤検出が少ない
  • Threshold Detector
    • フレームの輝度が一定以上/以下になったタイミングを検出できる

これらの複数を組み合わせて使うこともできますが、手持ちの動画だと Content-Aware DetectorAdaptive Content Detector では意図しないチャプターが大量に検出されてしまったため、今回は Threshold Detector のみを使いました。

さて、PySceneDetectでチャプター検出をするにはこのように書きます。

def detect_chapter(video_file_path: str) -> list[tuple[FrameTimecode, FrameTimecode]]:
    video = open_video(video_file_path)
    scene_manager = SceneManager()
    scene_manager.auto_downscale = True

    # detect White
    scene_manager.add_detector(ThresholdDetector(threshold=243, method=ThresholdDetector.Method.CEILING))

    # detect Black
    scene_manager.add_detector(ThresholdDetector(threshold=12, method=ThresholdDetector.Method.FLOOR))

    scene_manager.detect_scenes(frame_source=video, show_progress=True, frame_skip=1)
    chapter_list = scene_manager.get_scene_list()
    return chapter_list

上記の ThresholdDetector の2つの引数のうち、threshold はチャプターの区切りとみなす輝度のしきい値、methodthreshold で指定したしきい値の 以上(CEILING) or 以下(FLOOR) のどちらを区切りとみなすかを表しています。
上記のように書くことで、黒のフェードと白のフェードをどちらもチャプターの区切りとして検出しています。

各チャプターの動画を表示する

image.png

いくつかやり方はあると思いますが、今回は以下のような方法で実現しました。

  • Pythonから ffmpeg コマンドを叩いて、元の動画ファイルから各チャプターの動画を切り出して保存する
    • 5つのチャプターが検出されたら5つの動画ファイルができる
    • PySceneDetectに split_video_ffmpeg() というこれ用の関数があったのですが、動作が遅かったので自分で書きました
  • 各チャプターの動画ファイルを読み込んで表示する
    • 少し上に書いたやり方と同様

今回は各動画と合わせてチェックボックスも表示させたかったので、以下のように書きました。

splitted_video_grid = ft.GridView(
            runs_count=3, spacing=20, run_spacing=20, child_aspect_ratio=16 / 12, padding=ft.padding.only(right=20)
        )
check_box_list = [ft.Checkbox(value=False) for _ in range(len(splitted_video_list))]
for check_box, splitted_video in zip(check_box_list, splitted_video_list):
    splitted_video_grid.controls.append(
        ft.Container(
            ft.Column([check_box, splitted_video]),
            bgcolor=ft.colors.GREY_200,
            border_radius=10,
            padding=10,
        ),
    )

ちなみに、デスクトップアプリ上だと動画コンポーネントの見た目がとてもYoutubeっぽいのでクリックして再生/一時停止ができそうですが、現時点だとできません

選択されたチャプターを結合して保存する

これもいくつかやり方があると思いますが、上記と同様に ffmpeg を叩くようにしました。具体的には以下のような流れになっています。

  • Merge ボタンが押されたタイミングで各チェックボックスの状態(True/False)をみる
  • 一時ファイルを作成し、Trueになっている動画のファイルパスをすべて書き込む
  • ffmpeg concat コマンドでその一時ファイルを参照し、結合した動画を保存する

たとえば以下のようなテキストファイルを ffmpeg concat コマンドで読み込ませると、ファイル内の3つのファイルを結合して保存できます。

hoge/chapter_1.mp4
hoge/chapter_2.mp4
hoge/chapter_3.mp4

ffmpeg concat コマンドの注意点として動画の解像度を揃える必要があるのですが、今回は同じ動画ファイルから切り出してるので解像度はすべて同じです。

まとめ

以上を組み合わせると、好きな動画から好きなチャプターだけ保存できます。やったね!

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?