🍣

LLaVA-NeXTで作業動画から画像入りの手順書を作成してみる

2024/08/11に公開

はじめに

株式会社ファースト・オートメーションCTOの田中(しろくま)です!
生成AIによる文章生成が発展してく中で、入力に関して文章だけでなく様々な入力を扱えるようになってきています。音声や画像だけでなく、 GoogleのGemini 1.5 Proでは最大で1時間程度の動画を入力として扱うことができます。
弊社では製造業向けの生成AIを活用したプロダクト・サービス展開を行っており、この動画入力の技術を用いて、 製造現場での作業動画から作業手順書 を作るということを今回試してみたいと思います。
以下の2つのモデルで作業手順書の作成を行います。

前者はGoogleが提供しているクラウド環境で動く生成AIモデルです。
後者はByteDanceなどの研究者たちによって作られた オープンなマルチモーダル生成AIモデル で、動画を扱うことができます。
製造業ではセキュリティの関係でクラウド環境のサービスを使用できない ということも多く、ローカル環境で動かせるモデルとして、こちらのモデルも試してみたいと思います。

製造業における作業手順書とは?

製造業では作業者が変わっても製品を安定して作れるように作業手順書を作成します。
基本的にその会社のフォーマットに沿って作成されますが、手順を文章と画像によって作業の手順を説明するスタイルが一般的と思われます。

https://www.hancho.jp/column/manufacturing_work_instruction
https://biz.teachme.jp/blog/manuals-for-manufacturing/
https://activation-service.jp/iso/column/7476

AIによる作業手順書の作成

今回、作業手順書を作成するための動画をYoutubeから探して、以下のPC組み立て動画を使用することにしました。
https://www.youtube.com/watch?v=kaeVZ54SrUc

おおよそ8分くらいの動画になります。
作業書の生成は文書だけでなく、各説明にふさわしい画像を挿入できるように動画の中からフレームの抽出も行うようにします。

Geminiでの生成

まずはGeminiを用いて作業手順書を作成してみます。
以下のようなプロンプトを与えて手順書を作らせてみました。

この動画内の作業の手順書を作成してください。
手順書に作業中の画像を差し込んで見やすくしたいので、差し込む画像のフレームの時刻を書いてください。

実際に入力している様子は以下のようになります。

実行すると以下のような生成結果が得られました。

わかりました。動画内のPC組立作業の手順書を作成します。

## PC組立手順書

**必要なもの**

* マザーボード:ASUS ROG Crosshair VIII Dark Hero X570 (0:00)
* CPU:AMD Ryzen 9 5950X (0:03)
* メモリ:CORSAIR Vengeance RGB PRO SL 32GB (DDR4-3600) (0:45)
* SSD:Samsung 980 PRO 1TB x 2 (0:26)
* CPUクーラー:CORSAIR iCUE H150i ELITE CAPELLIX (0:26)
* 電源ユニット:ROG STRIX 850W (0:26)
* PCケース:CORSAIR 5000X (0:26)
* ドライバー 
* その他、必要なケーブル類、ネジ類


**手順**

1. **マザーボードの準備** (0:00)
    * マザーボードを箱の上に置く。
2. **CPUの取り付け** (0:00)
    * CPUソケットのレバーを上げて開く。 (1:21)
    * CPUの金色の三角形とソケットの三角形を合わせる。 (1:24)
    * CPUをソケットに落とし込み、レバーを閉じて固定する。 (1:30)
3. **メモリの取り付け** (0:45)
    * メモリソケットの両端のクリップを開く。 (2:08)
    * メモリの切り欠きとソケットの切り欠きを合わせる。 (2:09)
    * メモリをソケットに押し込み、クリップがカチッと音がするまでしっかり固定する。 (2:18)
    * 同じ手順でもう一方のメモリも取り付ける。
4. **SSDの取り付け** (0:26)
    * M.2スロットのカバープレートを外す。 (2:28)
    * SSDのサイズに合ったプラグアダプターを使用する。 (2:35)
    * SSDをスロットに斜めに差し込み、押し下げてネジで固定する。 (2:39)
    * カバープレートを元に戻す。 (2:42)
    * 同じ手順でもう一方のSSDも取り付ける。
5. **マザーボードをケースに取り付け** (3:00)
    * ケースのサイドパネルを外す。 (3:04)
    * ケースを横向きに置き、マザーボードの取り付け穴を確認する。 (3:07)
    * マザーボードをケースにスライドさせて取り付け、背面のI/Oパネルとケースの開口部を合わせる。 (3:11)
    * ネジでマザーボードを固定する。 (3:15)
6. **電源ユニットの取り付け** (0:26)
    * 電源ユニットに必要なケーブルを接続する。 (3:34)
    * 電源ユニットをケースの背面下部に取り付け、ファンがダストフィルター側に向いていることを確認する。 (3:43)
    * ネジを対角線上に交互に締め、均等に固定する。 (3:49)
7. **CPUクーラーの取り付け** (0:26)
    * ラジエーターにファンを取り付ける。 (4:14)
    * ラジエーターをケース上部に設置する。 (4:17)
    * ポンプヘッドの取り付け金具をマザーボードに対応するものに交換する。 (4:35)
    * ポンプヘッドとCPUの接触面に熱伝導グリスを塗布する。 (4:52)
    * ポンプヘッドをCPUに取り付け、ネジで固定する。 (5:02)
    * ポンプヘッドの向きを調整する。 (5:05)
8. **グラフィックカードの取り付け** (0:18)
    * ケース背面の拡張スロットカバーをグラフィックカードのサイズに合わせて外す。 (5:27)
    * グラフィックカードをPCIeスロットに押し込み、カチッと音がするまでしっかり固定する。 (5:24)
    * ネジでグラフィックカードを固定する。 (5:37)
9. **ケーブルの接続** (5:44)
    * 電源ユニットから出ている24ピンケーブルをマザーボードの右側に接続する。 (5:47)
    * CPU電源ケーブルをマザーボードの左上に接続する。 (5:51)
    * PCIe電源ケーブルをグラフィックカードに接続する。 (5:55)
    * ケースのフロントパネルのケーブル(電源ボタン、USB、オーディオなど)をマザーボードの下部に接続する。 (6:01)
    * ファンハブのケーブルをマザーボードに接続する。 (6:03)
    * 各ファンのケーブルをファンハブまたはマザーボードに接続する。
10. **電源ケーブルの接続** (6:18)
    * 電源ユニットに電源ケーブルを接続する。 
11. **動作確認** (6:29)
    * 電源を入れて、PCが起動することを確認する。
    * BIOS画面が表示されることを確認する。
12. **OSとドライバーのインストール** (6:51)
    * Windowsをインストールする。
    * マザーボード、グラフィックカード、その他のデバイスのドライバーをインストールする。
13. **ベンチマークテストの実行** (6:57)
    * UserBenchmarkなどのベンチマークテストを実行して、PCの性能を確認する。 (6:57)
14. **サイドパネルの取り付け** (7:12)
    * ケースのサイドパネルを取り付け、保護フィルムを剥がす。 (7:12)

これで、PCの組み立ては完了です。お疲れさまでした!

必要なパーツが最初にまとめられていたり、手順を大項目・小項目にまとめていたり、かなり精度高く生成されているのではないかと思います。
実際にいくつか抽出されている時刻での画像フレームを見てみると、違ってるのもありますが、おおよそちゃんと意味の合ったフレームを取り出せている感じがします。

CPUソケットのレバーを上げて開く。 (1:21)

◎合ってる

SSDをスロットに斜めに差し込み、押し下げてネジで固定する。 (2:39)

◎合ってる

ポンプヘッドとCPUの接触面に熱伝導グリスを塗布する。 (4:52)

◯ほぼ合ってる(実際に塗布しているシーンではない)

PCIe電源ケーブルをグラフィックカードに接続する。 (5:55)

△ケーブルは合ってる(実際に挿しているシーンではない)

UserBenchmarkなどのベンチマークテストを実行して、PCの性能を確認する。 (6:57)

❌違ってる

LLaVA-NeXTでの生成

単一画像が扱えるローカルLLMとしてLLaVAが有名かと思いますが、LLaVa-NexXTはLLaVAでは扱うことができなかった複数画像や動画を扱えるようにしたマルチモーダルLLMのモデルです。
扱える入力に応じていくつかモデルが別れており、主に以下の3つがあります。

  • LLaVA-OneVision
    • 複数画像、動画を扱える最新モデル(2024/08/06リリース)、0.5B/7B/72Bのパラメタサイズがある
  • LLaVA-NeXT-Video
    • 動画を扱えるモデル、7B/34B/32B-Qwenのパラメタサイズがある
  • LLaVA-NeXT-Interleave
    • 複数画像、動画、3Dを扱えるモデル、0.5B/7Bのパラメタサイズがある

今回、LLaVA-NeXT-Videoの34Bもしくは32B-Qwenあたりを試したいと考えていたのですが、これらをそのまま手元のPCで動かすのはスペック的に厳しかったので、AWS上でGPUインスタンスを立てて動かしました。
使用したインスタンスはg5.12xlargeインスタンスでVRAMが96GBあり、 32B-Qwenモデルだと80GBくらいVRAMを消費していました。 なので実際に今回のモデルを本当にローカルで動かすにはH100相当のGPU、もしくは複数のGPUを載せたPCを用意する必要がありそうです。

基本的なセットアップをドキュメントに書かれている手順で行い、用意されているデモ用のスクリプトにプロンプトが書かれているので、そこを編集しておきます。
LLaVA-NeXTは英語のほうが得意なようなので英語のプロンプトにしました。

diff --git a/scripts/video/demo/video_demo.sh b/scripts/video/demo/video_demo.sh
index 6f90e6a..6e5e5d3 100644
--- a/scripts/video/demo/video_demo.sh
+++ b/scripts/video/demo/video_demo.sh
@@ -1,5 +1,5 @@
 #!/bin/bash
-ROOT_DIR="/mnt/bn/vl-research/workspace/yhzhang/LLaVA-NeXT"
+ROOT_DIR="/home/ubuntu/LLaVA-NeXT"
 
 if [ ! -e $ROOT_DIR ]; then
     echo "The root dir does not exist. Exiting the script."
@@ -42,6 +42,6 @@ python3 playground/demo/video_demo.py \
     --mm_spatial_pool_mode ${POOL_MODE:-average} \
     --mm_pooling_position ${POOL_POSITION:-before} \
     --mm_newline_position ${NEWLINE_POSITION:-grid} \
-    --prompt "Please provide a detailed description of the video, focusing on the main subjects, their actions, the background scenes."
+    --prompt "Please create an instruction manual based on the tasks shown in this video. Since I want to make the manual easy to follow by including images of the work in progress, please note the timestamps of the frames where the images should be inserted."

このスクリプトを実行して得られた結果が以下になります。

Response: Here is a step-by-step guide to building a computer based on the tasks shown in the video:

1. Unboxing and inspecting components:
	* Unbox the ASUS ROG DARK Hero motherboard box, revealing stickers and documentation.
	* Unbox the AMD Ryzen 9 3rd Gen processor box, showing the processor inside.
	* Unbox the Corsair Vengeance LPX RAM module box, displaying the RAM module.
	* Unbox the Corsair H150i Elite Capellix liquid cooling kit box, showing the cooling kit inside.
2. Installing the processor:
	* Install the AMD Ryzen 9 3rd Gen processor into the motherboard socket using a screwdriver.
	* Apply thermal paste to the processor using a small tool.
3. Installing RAM:
	* Install the Corsair Vengeance LPX RAM module into the motherboard slot.
4. Assembling the case:
	* Assemble the black computer case by installing the front panel and securing it with screws.
	* Install the Corsair H150i Elite Capellix liquid cooling kit onto the motherboard.
	* Attach the liquid cooling tubes to the motherboard and processor.
	* Install the graphics card into the case.
	* Connect the cables to the motherboard and graphics card.
5. Testing the system:
	* Power on the computer and check the RGB lighting and system status on the monitor.
	* Give a thumbs-up gesture in front of the computer setup.

The images should be inserted at the following timestamps:
- Unboxing and inspecting components: 0:00 - 0:30
- Installing the processor: 0:31 - 0:50
- Installing RAM: 0:51 - 1:10
- Assembling the case: 1:11 - 1:50
- Testing the system: 1:51 - 2:30

Note: The timestamps are approximate and may vary slightly depending on the actual video length.

以下、日本語訳です。

回答: 以下は、ビデオに示されたタスクに基づくコンピュータを構築するためのステップバイステップのガイドです。

1. コンポーネントの開封と確認:
	* ASUS ROG DARK Hero マザーボードの箱を開封し、ステッカーやドキュメントを確認します。
	* AMD Ryzen 9 3rd Gen プロセッサーの箱を開封し、プロセッサーを確認します。
	* Corsair Vengeance LPX RAM モジュールの箱を開封し、RAM モジュールを確認します。
	* Corsair H150i Elite Capellix 液冷キットの箱を開封し、キットを確認します。
2. プロセッサーの取り付け:
	* AMD Ryzen 9 3rd Gen プロセッサーをドライバーを使用してマザーボードのソケットに取り付けます。
	* 小さなツールを使用してプロセッサーにサーマルペーストを塗布します。
3. RAM の取り付け:
	* Corsair Vengeance LPX RAM モジュールをマザーボードのスロットに取り付けます。
4. ケースの組み立て:
	* 黒いコンピュータケースを組み立て、フロントパネルを取り付けてネジで固定します。
	* Corsair H150i Elite Capellix 液冷キットをマザーボードに取り付けます。
	* 液冷チューブをマザーボードとプロセッサーに接続します。
	* グラフィックカードをケースに取り付けます。
	* ケーブルをマザーボードとグラフィックカードに接続します。
5. システムのテスト:
	* コンピュータの電源を入れ、モニターで RGB ライティングとシステム状態を確認します。
	* コンピュータのセットアップの前で親指を立てるジェスチャーをします。

画像は以下のタイムスタンプに挿入されるべきです:
- コンポーネントの開封と確認: 0:00 - 0:30
- プロセッサーの取り付け: 0:31 - 0:50
- RAM の取り付け: 0:51 - 1:10
- ケースの組み立て: 1:11 - 1:50
- システムのテスト: 1:51 - 2:30

注: タイムスタンプはおおよそのものであり、実際のビデオの長さに応じて若干の変動がある場合があります。

Geminiに比べると全体の説明や手順の数が少ないですが、いい感じに説明できている気がします。
あと見てもらうと分かるように、フレーム抽出で出ているフレームがかなり適当で、フレーム抽出まではうまくできなかったことが分かります。
この辺はいろいろとプロンプトを変えて試してみたのですが、全部0:00になってしまったりして、 LLaVA-NeXTではフレーム抽出まではうまくできないようでした。

LLaVA-NeXTではフレーム抽出ができなかったので、フレーム抽出は生成された文章を用いて、別の方法で行いたいと思います。
ここではシンプルに、CLIPを用いて動画のフレームと各説明文の近さを計算し、最も近いフレームを該当フレームとするというようにしました。
一応時系列データであるため、近いフレームが説明文の並びに対して前後しないなどを制約に入れるために、動的計画法を用いて最適化を行うようにしています。
以下がその計算を行うコードです。

import cv2
import torch
from PIL import Image
import open_clip
import numpy as np

# 動画パスとフレーム抽出間隔を設定
video_path = "playground/demo/pc_assembly_youtube_3.mp4"
interval = 5  # 秒

# OpenCLIPモデルと前処理の設定
device = "cuda" if torch.cuda.is_available() else "cpu"
model, _, preprocess = open_clip.create_model_and_transforms('ViT-B-32', pretrained='laion2b_s34b_b79k')
tokenizer = open_clip.get_tokenizer('ViT-B-32')
model.to(device)

def extract_frames(video_path, interval=1):
    cap = cv2.VideoCapture(video_path)
    frames = []
    frame_count = 0
    
    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break
        if frame_count % (interval * int(cap.get(cv2.CAP_PROP_FPS))) == 0:
            frames.append(frame)
        frame_count += 1
        
    cap.release()
    return frames

def encode_frames(frames):
    frame_features = []
    for frame in frames:
        image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        image = preprocess(image).unsqueeze(0).to(device)
        with torch.no_grad():
            frame_feature = model.encode_image(image)
        frame_features.append(frame_feature)
    return frame_features

def encode_texts(descriptions):
    text_features = []
    for description in descriptions:
        text = tokenizer(description).to(device)
        with torch.no_grad():
            text_feature = model.encode_text(text)
        text_features.append(text_feature)
    return text_features

def calculate_similarity(frame_features, text_features):
    similarities = []
    for text_feature in text_features:
        similarity = []
        for frame_feature in frame_features:
            sim = torch.cosine_similarity(text_feature, frame_feature).item()
            similarity.append(sim)
        similarities.append(similarity)
    return np.array(similarities)

def select_frames_with_dp(similarities):
    num_descriptions = similarities.shape[0]
    num_frames = similarities.shape[1]
    
    dp = np.full((num_descriptions, num_frames), -np.inf)
    path = np.full((num_descriptions, num_frames), -1, dtype=int)

    dp[0] = similarities[0]
    
    for i in range(1, num_descriptions):
        for j in range(i, num_frames):
            for k in range(i - 1, j):
                if dp[i - 1][k] + similarities[i][j] > dp[i][j]:
                    dp[i][j] = dp[i - 1][k] + similarities[i][j]
                    path[i][j] = k

    selected_frames = []
    max_index = np.argmax(dp[-1])
    for i in range(num_descriptions - 1, -1, -1):
        selected_frames.append(max_index)
        max_index = path[i][max_index]
    
    selected_frames.reverse()
    return selected_frames

# 解説文のリスト
descriptions = [
    "Unbox the ASUS ROG DARK Hero motherboard box, revealing stickers and documentation.",
    "Unbox the AMD Ryzen 9 3rd Gen processor box, showing the processor inside.",
    "Unbox the Corsair Vengeance LPX RAM module box, displaying the RAM module.",
    "Unbox the Corsair H150i Elite Capellix liquid cooling kit box, showing the cooling kit inside.",
    "Install the AMD Ryzen 9 3rd Gen processor into the motherboard socket using a screwdriver.",
    "Apply thermal paste to the processor using a small tool.",
    "Install the Corsair Vengeance LPX RAM module into the motherboard slot.",
    "Assemble the black computer case by installing the front panel and securing it with screws.",
    "Install the Corsair H150i Elite Capellix liquid cooling kit onto the motherboard.",
    "Attach the liquid cooling tubes to the motherboard and processor.",
    "Install the graphics card into the case.",
    "Connect the cables to the motherboard and graphics card.",
    "Power on the computer and check the RGB lighting and system status on the monitor.",
    "Give a thumbs-up gesture in front of the computer setup.",
]

# 各ステップを実行
frames = extract_frames(video_path, interval)
frame_features = encode_frames(frames)
text_features = encode_texts(descriptions)
similarities = calculate_similarity(frame_features, text_features)
selected_frames_indices = select_frames_with_dp(similarities)

# 選択されたフレームを取得
selected_frames = [frames[idx] for idx in selected_frames_indices]

# 選択されたフレームを保存または表示する例
for i, frame in enumerate(selected_frames):
    cv2.imwrite(f"selected_frame_{i}.png", frame)

print("フレーム抽出と選択が完了しました。")

実際に抽出されたフレームをいくつか見てみます。
(上のスクリプトでフレームのサンプリングが5秒なのでそもそも良いフレームが見つからなかったものもありそうです。)

Unbox the AMD Ryzen 9 3rd Gen processor box, showing the processor inside.

◯合ってそう

Install the AMD Ryzen 9 3rd Gen processor into the motherboard socket using a screwdriver.

❌違ってる(CPUではなく、SSDのカバーを外しているところ)

Assemble the black computer case by installing the front panel and securing it with screws.

❌違ってる

Power on the computer and check the RGB lighting and system status on the monitor.

◯まあ合ってそう

フレーム抽出はCLIPを使って別途行うことで、それなりには抽出できることが分かりました。

まとめ

GeminiとLLaVA-NeXTを使って動画から作業手順書を作ってみました。
フレーム抽出もできるGeminiはかなりすごいモデルだと思いましたが、ローカルで使えるLLaVA-NeXTも工夫次第ではいろいろと使えそうな感じがします。

最後までお読みいただきありがとうございました。
最後に宣伝ですが、株式会社ファースト・オートメーションは一緒に働いて下さる仲間を絶賛募集中です!

  • ローカルLLMを使ったプロダクトに興味がある
  • LLMの社会実装に貢献したい
  • 製造業をより良くしたい

といったことに少しでも興味がある方、ぜひ下記応募リンクからご連絡下さい!
https://www.notion.so/firstam/5d68308315da46e7b0a7e02742338188

株式会社ファースト・オートメーション

Discussion