🔥

AIでInstagramリールを分析・シナリオを生成する技術的挑戦

に公開

はじめに

こんにちは、MOSHのマーケットチーム(前AI market)のAndyです。
今回は、以前MOSHで短期的にベータ提供していた「AIによるInstagramリール分析・シナリオ生成機能」について、その技術的な裏側をお話ししようと思います。

この機能は、ユーザーがアップロードした動画を元にAIが動画の構成を分析し、さらにその構成を活かして別のテーマで新しいシナリオを生成するというものでした。残念ながら現在はクローズしていますが、開発の過程で得られた知見は非常に興味深いものだったので、ここで共有させていただきます。

この記事では、主に以下の2つの機能のバックエンド実装に焦点を当てて解説します。

リール分析機能 (reel-analysis): s3から動画を取得し、内容を分析して構造化データ(台本)を生成する

リール作成機能 (reel-maker): 分析済みの台本を元に、新しいテーマでシナリオを再生成する

なぜやるのか

多くのクリエイターにとって、人気のInstagramリールを参考にすることはコンテンツ制作の重要なプロセスです。しかし、「どの部分が視聴者の興味を引いているのか」「どのような構成になっているのか」を言語化して分析するのは簡単ではありません。また、良い構成を見つけても、それを自分のテーマに落とし込んで一からシナリオを作るのは大変な作業です。

そこで私たちは、このプロセスをAIで自動化し、クリエイターがより効率的に質の高いコンテンツを制作できるよう支援したいと考えました。具体的には、

参考リール動画をアップロードするだけで、時間軸に沿った場面ごとの説明やポイントをまとめた「分析台本」を自動生成する。

その分析台本をベースに、例えば「親子丼」のレシピ動画を「カツ丼」のレシピ動画のシナリオに変換するなど、新しいテーマのシナリオを瞬時に作成する。

という2つの体験を提供することを目指しました。

どうやるのか:リール分析 (reel-analysis) の実装

それでは、リール分析機能の裏側を、処理の流れに沿って見ていきましょう。
機能全体は、MOSHの既存の非同期タスク処理基盤(/generations API)上で実装されています。

Step 1: 動画からキーフレームを抽出する

ユーザーが動画をアップロードし、S3に保存する処理についてはここでは省略しますが、S3から動画データを取得できたら、まずはAIが分析しやすいように静止画の連続、つまりキーフレームに分解します。この処理は、ffmpegのラッパーであるVideoServiceにカプセル化しました。

generate_reel_analysisからは、以下のようにVideoServiceを呼び出しています。

# generate_reel_analysis より抜粋

# 動画サービスを呼び出し、キーフレームを抽出・保存
thumbnail_files = self.__video_service.save_captures(
    input_path=video_path,
    output_folder=str(Path(output_path).parent),
)

このsave_capturesメソッドの実装を見てみましょう。Pythonからffmpegを非常に直感的に操作できる ffmpeg-python というライブラリを利用しています。.filter("fps", fps=1) のようにメソッドチェーンで処理を記述できるのが特徴です。ここでは「1秒あたり1フレーム (fps=1)」を「高品質 (quality=2)」で抽出しています。

# VideoService.py

class VideoService:
    @staticmethod
    def save_captures(
        input_path: str,
        output_folder: str,
        fps: int = 1,
        quality: int = 2,
        # ...
    ) -> List[Path]:
        # ...
        try:
            # fps(1秒あたりのフレーム数)に基づいて均等にフレームを抽出
            ffmpeg.input(input_path).filter("fps", fps=fps).output(
                output_path, **{"qscale:v": quality}
            ).overwrite_output().run(capture_stdout=True, capture_stderr=True)
        except ffmpeg.Error as e:
            raise ValueError(f"Failed to generate video screenshots: {e.stderr}") from e
        # ...

どうやるのか:AIインターフェースの裏側

さて、動画とキーフレームが準備できたところで、いよいよAIの出番です。
AIモデルとのやり取りもAiClientというgRPCサービサーで抽象化しています。

# AiClient.py

class AiClient(AiClientServicer):
    @inject
    def __init__(
        self,
        open_ai_service: OpenaiService,
        dify_service: DifyService,
        gemini_service: GeminiService,
        # ...
    ):
        self.__open_ai_service = open_ai_service
        self.__dify_service = dify_service
        self.__gemini_service = gemini_service
        # ...

AiClientは、用途に応じてOpenaiServiceDifyServiceGeminiServiceといった具体的なサービスクラスを使い分けます。これにより、例えば「このタスクはコストの安いGPT-3.5で」「この動画分析タスクは高性能なGeminiで」といった柔軟な選択が可能になります。

Step 2: AIによる分析 (CreateReelAnalysis)

AiClientがリール動画を分析する際の中核メソッドが CreateReelAnalysis です。

当初、このリール分析処理は後述するシナリオ生成機能と同様に、LLMワークフロープラットフォームであるDifyを使って実装する予定でした。しかし、Difyへ動画ファイルをアップロードする際に原因不明のエラーが発生し、安定した処理が難しいことが判明しました。そこで、私たちはアプローチを変更し、GeminiのマルチモーダルAPIを直接呼び出す形で実装することにしました。これにより、安定性を確保しつつ、詳細なプロンプトで出力を精密にコントロールすることが可能になりました。

# AiClient.py

def CreateReelAnalysis(
    self,
    request: ai_proto.CreateReelAnalysisRequest,
    context: Optional[Any] = None,
) -> ai_proto.CreateReelAnalysisResponse:
    if not request.reel_data or not request.reel_name:
        raise ValueError

    try:
        # Geminiサービスを使用して動画分析を処理
        # 1. GeminiAPIにファイルをアップロード
        file_data = self.__gemini_service.upload_file(
            file_input=request.reel_data,
            file_name=request.reel_name,
        )

        if file_data is None:
            raise ValueError("GeminiAPIにファイルをアップロードできません")

        # 2. Geminiでコンテンツを生成
        prompt = """
        <指示>
        インスタリールの詳細な分析を構造化して行って、解説とプロットを出力してください。
        応答文を省略して出力
        </指示>

        <中間書式>
        ## 解説:
        下記の項目を必ず出力してください。
        ・フック
        ・CTA
        ・コンテンツの概要
        ...(中略)...
        ## プロット:
        場面転換ごとにカット割りをリールのメッセージをリバース・エンジニアリングして構造化して出力してください。
        1カット最大5秒で区切って、下記の書式で1カット1行ずつ出力してください。
        ...(中略)...
        </中間書式>

        <出力書式>
        下記のJSON schemaに準拠したJSONを出力してください。
        {
          "$schema": "http://json-schema.org/draft-07/schema#",
          "type": "object",
          "properties": { "explanation": { ... }, "transcript": { ... }, "plot": { ... } },
          "required": [ "explanation", "plot", "transcript" ]
        }
        </出力書式>

        <制約>
        書式を厳守。
        </制約>
        """

        text_content = self.__gemini_service.complete(
            model="gemini-2.0-flash-exp",
            text=prompt,
            file_data=file_data,
            # ...パラメータ設定...
        )
        
        # Gemini APIレスポンスを解析してフォーマット
        text = text_content.strip()
        if text.startswith("```json"):
            text = text[7:]
        # ...(レスポンスの整形処理)...
        outputs = json.loads(text.strip())

    except Exception as e:
        self.__logger.error(e)
        return ai_proto.CreateReelAnalysisResponse()

    return ai_proto.CreateReelAnalysisResponse(result=dict_to_struct(outputs or {}))

ここでの最大のポイントは、詳細かつ厳格なプロンプトです。
AI、特にLLMから安定して望んだ形式の出力を得るためには、「何をしてほしいか」を曖昧さなく伝える必要があります。私たちは、

<指示>: 全体の目的を簡潔に伝える。

<中間書式>: AIが思考する上での思考の道筋(出力してほしい項目)を具体的に指示する。

<出力書式>: 最終的なアウトプットをJSON Schemaで厳密に定義する。

という3段構えのプロンプトを設計しました。これにより、AIの出力がブレることなく、常にパース可能なJSON形式で返ってくる確率を格段に高めています。また、レスポンスに json ... のようなMarkdownのコードブロックが含まれてしまうケースも想定し、それらを適切に除去する後処理も実装しています。これらは、AIをプロダクトに組み込む上での実践的なノウハウです。

これで、「分析台本」の完成です。

どうやるのか:シナリオ生成 (reel-maker) の実装

分析機能 (reel-analysis) で台本ができたので、今度はそれを元に新しいテーマでシナリオを生成する reel-maker の実装を見ていきましょう。こちらは比較的シンプルです。

# AiClient.py

def CreateReelMaker(
    self,
    request: ai_proto.CreateReelMakerRequest,
    context: Optional[Any] = None,
) -> ai_proto.CreateReelMakerResponse:
    if not request.reel_json or not request.theme:
        raise ValueError

    try:
        # Difyのワークフローを使ってシナリオを生成
        outputs = self.__dify_service.run_workflows(
            app_name=DifyAppName.ReelMaker,
            inputs={
                "reel_json": json.dumps(struct_to_dict(request.reel_json)),
                "theme": request.theme,
            },
        )
    except Exception as e:
        self.__logger.error(e)
        return ai_proto.CreateReelMakerResponse()

    return ai_proto.CreateReelMakerResponse(result=dict_to_struct(outputs or {}))

ポイントは、AIにリクエストを送る前に、元の分析台本から動画や画像のID (thumbnailIds, videoId) を取り除き、テキスト情報だけを渡している点です。AIには、動画の構成や流れといったテキスト情報だけをインプットとして与え、「この構成を真似て、新しいテーマでシナリオを作って」とお願いするわけです。こちらは動画ファイルではなくJSONテキストを扱うため、当初の予定通りDifyのワークフローで安定して実装できました。

これにより、AIは元の動画の視覚情報に惑わされることなく、純粋にシナリオの構造だけを参考に新しいアウトプットを生成することができます。

おわりに

今回は、過去に開発したAIリール分析・シナリオ生成機能の技術的な側面を、コードを交えながらご紹介しました。

アップロードした動画をffmpegでキーフレームに分解

AiClientを介して、用途に応じて複数のAIサービスを使い分け。当初Difyで実装予定だった動画分析がエラーで頓挫し、急遽Geminiの直接呼び出しに切り替えるなど、現実的な課題解決も行った。

厳格なプロンプトエンジニアリングとJSON Schemaにより、AIから安定した構造化データを取得

分析台本のテキスト情報だけを使い、AIに別テーマのシナリオを再生成させる

一連の流れは、外部の動的なコンテンツを安定して処理し、AIが扱いやすい形式に加工するという、多くの技術的な課題と工夫の連続でした。

この機能自体はクローズとなりましたが、ここで得られた非同期処理のノウハウや、外部サービスと連携する際の注意点、AIへの効果的なデータ入力形式の検討といった経験は、チームの貴重な財産となっています。
MOSHでは、これからもクリエイターの皆さんの活動をテクノロジーで支援できるよう、様々な挑戦を続けていきます。

最後までお読みいただき、ありがとうございました!

MOSH

Discussion