PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

Strands AgentsでAIに動画編集をさせてみた

みなさんこんにちは。2025年4月に新卒として入社しました、メディアサプライチェーン技術部第二グループの坂本です。

突然ですが、自然言語だけでAIが勝手に動画編集をしてくれたら便利だと思いませんか? 今回は、AWSが開発したオープンソースのAIエージェントSDK、Strands Agentsを使って、自然言語による指示だけでAIに動画編集をしてもらうシステムを構築していきたいと思います。

Strands Agentsとは?

Strands Agentsは、AWSが開発したオープンソースのAIエージェントSDKで、以下のような特徴を持っています。

  • モデル駆動のオーケストレーション: LLMの推論能力を活用して、タスクの計画・実行・振り返りを自律的に行う。
  • モデル・プロバイダー非依存: Amazon Bedrock、OpenAI、Anthropic、ローカルモデルなど、様々なLLMプロバイダーに対応。コードを変更せずにプロバイダーを切り替え可能。
  • シンプルなマルチエージェント機能: ハンドオフ、スウォーム、グラフワークフローなどのマルチエージェント構成を簡単に実装でき、Agent-to-Agent(A2A)通信もサポート。
  • AWSとの統合: Amazon BedrockやAWSの各種サービスとシームレスに連携可能。

strandsagents.com

実際に試してみる

例えば、最小限これだけのコードでエージェントが動かせてしまいます。

# ライブラリの追加

# pipの場合
pip install strands-agents
# uvの場合
uv add strands-agents
from strands import Agent

# デフォルト設定のエージェントを初期化
agent = Agent()

# エージェントにプロンプトを渡す
agent("コンニチハ!")
$ uv run python greeting.py
こんにちは!(Konnichiwa!)

半角カタカナで挨拶していただいて、ありがとうございます。何かお手伝いできることはありますか?

(Thank you for greeting me in half-width katakana! Is there anything I can help you with?)

Strands Agents自体は無料で使用できますが、使用するモデルによって従量課金が発生するため注意してください。

※ 事前にAWS CLI等で認証設定やモデルの有効化が必要です。

Amazon Bedrock - Strands Agents

また、@toolデコレータを用いて作成した関数を渡すと、そのツールを使ってタスクを実行してくれます。今回は、試しにAmazon BedrockのClaude Sonnet 4を使ってみましょう。

from strands import Agent, tool
from strands.models import BedrockModel

@tool
def hello_world_tool() -> str:
    """
    単純に "Hello, World!" をprintするツール
    """
    print("Hello World!")

bedrock_model = BedrockModel(
    model_id="apac.anthropic.claude-sonnet-4-20250514-v1:0"
)

agent = Agent(
    # 定義したツールをエージェントに渡す
    tools=[hello_world_tool],
    model=bedrock_model
)

if __name__ == "__main__":
    prompt = "hello_world_toolを使って挨拶をしてください。"
    response = agent(prompt)

こちらのコードを実行すると、以下のような返事を返してくれます。

$ uv run python tool_sample.py
hello_world_toolを使って挨拶いたします。
Tool #1: hello_world_tool
Hello World!
hello_world_toolを実行しました。このツールは「Hello, World!」を出力する機能を持っています。挨拶が完了いたしました!

このように、自作したツールを渡すと指示に応じて適切に使用してくれるため、高い自由度でエージェントの構築ができてしまいます。

実装

では、実際にStrands Agentsで、簡単な動画編集するコードを作成してみましょう。

編集手順

まず、想定する編集手順を考えましょう。assets/ 配下に編集用の動画や画像のアセットを配置しておき、エージェントには配置したアセットを使用して、編集をしてもらうことにします。

  1. assets/bbb.movの冒頭部、0から20秒までを切り抜き、assets/bbb_20sec.mp4として保存する。

  2. 保存したassets/bbb_20sec.mp4のタイトルロゴ部分(0~4.4秒まで)に、蓋用の画像(assets/futa.png)を挿入し、assets/bbb_futa.mp4として保存する。

  3. 2で編集、保存した動画(assets/bbb_futa.mp4)の先頭に、jingle.mp4をジングル動画として結合し、保存する。

assets/フォルダに配置したアセット(クリックで展開)

assets/bbb.mov *1

assets/futa.png

assets/jingle.mp4 (Veo 3.1で生成)

ツールの作成

次に、上で定義した編集手順の各作業が実行できるように、編集に最低限必要なツールを定義していきましょう。今回は、以下の4つのツールを作成します。

ツール名 説明
list_files 編集に使用するフォルダ内のファイル一覧を取得
concat_videos 複数の動画を順番に連結
insert_image 指定区間を画像に置換
trim_video 動画を開始・終了位置を指定してトリミング

実装

それでは、定義したツールを使って実際にコードを書いていきましょう。 プロジェクトの構成は以下の通りです。

strands-agent-blog/
├── agent.py          # エージェント本体
├── tools.py          # 動画編集ツールの定義
├── pyproject.toml    # プロジェクト設定
├── .python-version   # pythonバージョン
└── assets/           # 素材・出力フォルダ
    ├── bbb.mov              # 素材動画(Big Buck Bunny)
    ├── futa.png             # 蓋用画像
    └── jingle.mp4           # ジングル動画

まず、エージェントに渡すためのツールを作成します。

tools.py

from strands import tool
from moviepy import VideoFileClip, concatenate_videoclips, ColorClip, ImageClip
from pathlib import Path

WORK_DIR = Path("assets")

# ディレクトリ作成
WORK_DIR.mkdir(exist_ok=True)


# --- ユーティリティ関数 ---

def _find_video(name: str) -> Path | None:
    """assets/配下で動画ファイルを探す"""
    video_extensions = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
    path = WORK_DIR / name
    if path.exists() and path.suffix.lower() in video_extensions:
        return path
    return None


def _find_image(name: str) -> Path | None:
    """assets/配下で画像ファイルを探す"""
    image_extensions = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}
    path = WORK_DIR / name
    if path.exists() and path.suffix.lower() in image_extensions:
        return path
    return None


# --- ツール定義 ---

@tool
def list_files() -> list[dict]:
    """
    assets/フォルダ内の全ファイル一覧を取得します。
    
    Returns:
        ファイル情報のリスト(name, size_mb, duration_sec)
    """
    video_extensions = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
    files = []
    
    for file_path in WORK_DIR.iterdir():
        if not file_path.is_file():
            continue
        
        file_info = {
            "name": file_path.name,
            "size_mb": round(file_path.stat().st_size / (1024 * 1024), 2),
        }
        
        # 動画の場合は再生時間を取得
        if file_path.suffix.lower() in video_extensions:
            try:
                with VideoFileClip(str(file_path)) as clip:
                    file_info["duration_sec"] = round(clip.duration, 2)
            except Exception:
                file_info["duration_sec"] = None
        
        files.append(file_info)
    
    return files


@tool
def concat_videos(video_names: list[str], output_name: str) -> str:
    """
    複数の動画を順番に連結して1つのファイルに出力します。
    
    Args:
        video_names: 連結する動画ファイル名のリスト(assets/内のファイル名)
        output_name: 出力ファイル名(拡張子なし)
    
    Returns:
        出力されたファイルのパス
    """
    clips = []
    
    try:
        for name in video_names:
            path = _find_video(name)
            if path is None:
                return f"エラー: {name} が見つかりません"
            
            clip = VideoFileClip(str(path))
            clips.append(clip)
        
        final_clip = concatenate_videoclips(clips, method="compose")
        
        output_path = WORK_DIR / f"{output_name}.mp4"
        final_clip.write_videofile(str(output_path), codec="libx264", audio_codec="aac")
        
        return f"成功: {output_path} に保存しました({round(final_clip.duration, 2)}秒)"
    
    finally:
        for clip in clips:
            clip.close()
        if 'final_clip' in locals():
            final_clip.close()


@tool
def insert_image(video_name: str, image_name: str, start: float, end: float, output_name: str) -> str:
    """
    動画の指定区間(start〜end秒)を画像に置換します。
    
    Args:
        video_name: 入力動画ファイル名(assets/内)
        image_name: 挿入する画像ファイル名(assets/内、png/jpg/gif等)
        start: 画像挿入開始時間(秒)
        end: 画像挿入終了時間(秒)
        output_name: 出力ファイル名(拡張子なし)
    
    Returns:
        出力されたファイルのパス
    """
    video_path = _find_video(video_name)
    if video_path is None:
        return f"エラー: {video_name} が見つかりません"
    
    image_path = _find_image(image_name)
    if image_path is None:
        return f"エラー: {image_name} が見つかりません"
    
    clip = None
    image_clip = None
    final_clip = None
    
    try:
        clip = VideoFileClip(str(video_path))
        
        if start < 0 or end > clip.duration or start >= end:
            return f"エラー: 無効な時間範囲です (動画長: {clip.duration}秒)"
        
        # 画像クリップを作成(動画サイズにリサイズ)
        image_clip = ImageClip(str(image_path), duration=end - start)
        image_clip = image_clip.resized(clip.size)
        
        # 音声を無音に
        if clip.audio:
            image_clip = image_clip.with_audio(None)
        
        parts = []
        if start > 0:
            parts.append(clip.subclipped(0, start))
        parts.append(image_clip)
        if end < clip.duration:
            parts.append(clip.subclipped(end, clip.duration))
        
        final_clip = concatenate_videoclips(parts, method="compose")
        
        output_path = WORK_DIR / f"{output_name}.mp4"
        final_clip.write_videofile(str(output_path), codec="libx264", audio_codec="aac")
        
        return f"成功: {output_path} に保存しました({start}秒〜{end}秒に{image_name}を挿入)"
    
    finally:
        if clip:
            clip.close()
        if image_clip:
            image_clip.close()
        if final_clip:
            final_clip.close()


@tool
def trim_video(video_name: str, start: float, end: float, output_name: str) -> str:
    """
    動画を指定した開始・終了位置でトリミングします。
    
    Args:
        video_name: 入力動画ファイル名(assets/内)
        start: トリミング開始時間(秒)
        end: トリミング終了時間(秒)
        output_name: 出力ファイル名(拡張子なし)
    
    Returns:
        出力されたファイルのパス
    """
    path = _find_video(video_name)
    if path is None:
        return f"エラー: {video_name} が見つかりません"
    
    clip = None
    trimmed_clip = None
    
    try:
        clip = VideoFileClip(str(path))
        
        if start < 0 or end > clip.duration or start >= end:
            return f"エラー: 無効な時間範囲です (動画長: {clip.duration}秒)"
        
        trimmed_clip = clip.subclipped(start, end)
        
        output_path = WORK_DIR / f"{output_name}.mp4"
        trimmed_clip.write_videofile(str(output_path), codec="libx264", audio_codec="aac")
        
        return f"成功: {output_path} に保存しました({start}秒〜{end}秒を切り出し、{round(trimmed_clip.duration, 2)}秒)"
    
    finally:
        if clip:
            clip.close()
        if trimmed_clip:
            trimmed_clip.close()

# ツール一覧をエクスポート
EDIT_TOOLS = [
    list_files,
    concat_videos,
    insert_image,
    trim_video,
]

そして、作成したツールをエージェントに渡し、自然言語で動画編集ができるアシスタントを構築します。

agent.py

from strands import Agent
from tools import EDIT_TOOLS
from strands.models import BedrockModel

SYSTEM_PROMPT = """あなたは動画編集アシスタントです。
ユーザーの自然言語による指示に従い、ツールを使って動画を編集します。

利用可能なツール:
1. list_files: assets/フォルダ内の全ファイル一覧を取得
2. concat_videos: 複数動画を順番に連結
3. insert_image: 指定区間を画像に置換
4. trim_video: 動画を指定した開始・終了位置でトリミング

作業フォルダ:
- assets/: 素材・中間生成物・出力すべてをここで管理

作業手順:
1. まずlist_filesで利用可能な素材を確認
2. 必要な編集を実行

注意事項:
- すべてのファイルはassets/フォルダ内で管理
- 保存前にassets/フォルダ内をlist_filesで確認
- 時間は秒単位で指定
"""

bedrock_model = BedrockModel(
    model_id="apac.anthropic.claude-sonnet-4-20250514-v1:0"
)

agent = Agent(
    system_prompt=SYSTEM_PROMPT,
    tools=EDIT_TOOLS,
    model=bedrock_model
)

if __name__ == "__main__":
    ## コンソール上で対話可能にする
    while True:
        prompt = input("\n編集指示> ").strip()
        if prompt.lower() in ("quit", "exit", "q"):
            break
        if not prompt:
            continue

        response = agent(prompt)

これだけで、自然言語の指示から適切なツールを選択・実行してくれるエージェントの完成です。

実演

それでは、実際に実装したコードを動かしてみましょう!
agent.pyを実行し、編集指示を入力します。

❯ uv run python agent.py

編集指示>
1. assets/bbb.movを、0秒から20秒までをトリミングして、assets/bbb_20sec.mp4として保存する
2. bbb_20sec.mp4の最初から4.400秒までassets/futa.pngを挿入 assets/bbb_futa.mp4として保存する
3. jingle.mp4とassets/bbb_futa.mp4を繋げて、assets/jingle_and_bbb_futa.mp4として保存する

まず、assets/フォルダ内の利用可能なファイルを確認します。
Tool #1: list_files
ファイルが確認できました。それでは、指示された手順に従って編集を行います。

1. まず、bbb.movを0秒から20秒までトリミングします:
Tool #2: trim_video
MoviePy - Building video assets/bbb_20sec.mp4.
MoviePy - Writing audio in bbb_20secTEMP_MPY_wvf_snd.mp4
MoviePy - Done.

MoviePy - Writing video assets/bbb_20sec.mp4

MoviePy - Done !

MoviePy - video ready assets/bbb_20sec.mp4
2. 次に、bbb_20sec.mp4の最初から4.400秒までfuta.pngを挿入します:
Tool #3: insert_image
MoviePy - Building video assets/bbb_futa.mp4.
MoviePy - Writing audio in bbb_futaTEMP_MPY_wvf_snd.mp4
MoviePy - Done.

MoviePy - Writing video assets/bbb_futa.mp4

MoviePy - Done !

MoviePy - video ready assets/bbb_futa.mp4
3. 最後に、jingle.mp4とbbb_futa.mp4を繋げます:
Tool #4: concat_videos
MoviePy - Building video assets/jingle_and_bbb_futa.mp4.
MoviePy - Writing audio in jingle_and_bbb_futaTEMP_MPY_wvf_snd.mp4
MoviePy - Done.

MoviePy - Writing video assets/jingle_and_bbb_futa.mp4
MoviePy - Done !
MoviePy - video ready assets/jingle_and_bbb_futa.mp4
すべての編集が完了しました!最終的な結果をまとめると:

1. ✅ `bbb.mov`を0-20秒でトリミング → `bbb_20sec.mp4` (20秒)
2. ✅ `bbb_20sec.mp4`の0-4.4秒に`futa.png`を挿入 → `bbb_futa.mp4` (20秒)
3. ✅ `jingle.mp4`と`bbb_futa.mp4`を連結 → `jingle_and_bbb_futa.mp4` (28.17秒)

最終的な動画ファイル `assets/jingle_and_bbb_futa.mp4` が完成しました。

実際に動いている様子

編集が終了したようです!それでは実際に編集された動画ファイルを確認してみましょう!

完成した動画

編集指示通りの結果になっていますね!

終わりに

今回はStrands Agentsを使って、自然言語で動画編集ができるAIエージェントを構築しました。

今回作成したツールは最低限のものでしたが、コーディングエージェントなどを用いてツールを追加してさらに高度な編集をさせたり、マルチエージェントで複数の動画を編集したりもできてしまいます。 皆さんもぜひ、お手元のエージェントをアレンジして、自分だけの動画編集AIを作成してみてください!

*1: (c) copyright 2008, Blender Foundation / www.bigbuckbunny.org