AWS を駆使して動画に YouTuber 感をちょい足しするソリューションを作ってみる

2020-08-03
日常生活で楽しむクラウドテクノロジー

Author : 今村 優太

こんにちは、今村です。普段は AWS Japan で、ソリューション アーキテクトという仕事をしています。

突然ですが、みなさん、YouTube はお好きですか ?

私は、最近家にいる時間が増えたこともあり自炊にハマっているのですが、いろんな料理動画を YouTube で見て参考にしています。好きなお料理 YouTuber は、バズレシピのリュウジさんや、モノマネで有名なとっくんさんです。レシピ本も買いました。

img_transcribe_youtuber_01

しかし、YouTube でさまざまな動画を見ていくうちに、あることが気になってきました。それが、数多ある YouTube 動画の「カット」の方法です。いわゆる YouTuber の方々の動画って、無音の時間が全然ないですよね。非常に文章で表現しづらいですが、ある言葉を喋り終わったら、コマ飛びしたように次の場面に移り、次の言葉を喋り始める、というような編集がされている場面をよく見ませんか ? 無音の時間を切り詰めて、できるだけセリフを敷き詰めた動画をよく目にします。

調べてみると、これはどうやら「ジェットカット」(もしくはジャンプカット) と呼ばれる編集手法のようです。間延びしてしまった無音の箇所を削って、決まった時間内に可能な限り情報を詰め込むことで、動画のテンポを良くすることが目的とのこと。たしかに、あまりに無音の時間が長いと、動画の途中で離脱されてしまうかもしれないですし、YouTube という環境では合理的といえるかもしれません。

そこで今回は、AWS のサービスを駆使して、動画を "自動的に"「ジェットカット」するソリューションを作り、動画を「YouTuber っぽく」することにトライしてみました。


トライした結果 👀

細かいことはさておき、まずはできあがった動画をご覧ください。

今回は、私と同じくソリューション アーキテクトとして働く高橋 敏行さんの動画をお借りしました (Special Thanks to Yukky-san !)。

30 秒ほどの動画に対して、

  • 元の動画
  • 今回のソリューションでジェットカットを適用してみた動画
  • さらにオマケとして、手動で字幕や SE などの編集を足してみた動画

を 3 つ繋げたものがこちらです。

デモ動画

いかがでしょうか。最後の編集でゴリ押した感じは否めませんが、ジェットカットにより無音部分がいい感じにカットされ、憧れの YouTuber に一歩近づけたんじゃないでしょうか。なお、最後の編集は自分でやってみたのですが、動画の編集を初めてやってみて、その地道さを痛感しました。これを毎日やってるのはすごい。


実現方法を考えてみる 🤔

今回、ジェットカットの自動化を AWS で実現するにあたり、どのような機能が利用できるか、あらためて考えてみました。検討してみたところ、比較的最近リリースされた "2 つの新機能" が使えそうだと思い至りましたので、ご紹介します。

Amazon Transcribe

まず利用する AWS のサービスが Amazon Transcribe です。Amazon Transcribe は、音声や動画ファイルを入力すると、音声データをもとに発話した内容をテキストへ書き起こすサービスです。2019 年 11 月には新たに日本語に対応しました。

個人的に、このサービスの注目ポイントは 2 つです。1 つは "動画ファイルにも対応" していること。そしてもう 1 つが、"発話したタイミング (時刻) も一緒に取得できる" ことです。以下のデータは、Transcribe に対して、とある動画ファイルを入力してみた結果の一部抜粋です。

{
  "items":[{
    "start_time":"4.06",
    "end_time":"4.21",
    "alternatives":[{
      "confidence":"0.831",
      "content":"みな"
    }],
    "type":"pronunciation"
  },{
    "start_time":"4.21",
    "end_time":"4.36",
    "alternatives":[{
      "confidence":"0.8416",
      "content":"さん"
    }],
    "type":"pronunciation"
  },{
    "start_time":"4.36",
    "end_time":"4.82",
    "alternatives":[{
      "confidence":"1.0",
      "content":"こんにちは"
    }]
  },{
    "start_time":"5.23",
    "end_time":"5.81",
    "alternatives":[{
      "confidence":"1.0",
      "content":"アマゾンウェブサービス"
    }]
  }]
}

このように、テキストが書き起こされるとともに、発話した時間と終了した時間が、10 ミリ秒の粒度で得られます。また、よくよく見てみると、「こんにちは」の end_time と「アマゾンウェブサービス」の start_time のあいだには、0.41 秒ほどの間が空いていることが分かります。

ジェットカットを実現するには、このような間が空いている区間を取り除けば、それっぽいものができそうです。

AWS Lambda

続いて利用するサービスが AWS Lambda です。AWS Lambda はサーバーレスのコンピューティング サービスで、サービスへデプロイした任意のコードを実行することができます。ご存知の方も多いかと思いますので、今回は Lambda 自体の説明は割愛します。

AWS Lambda は 2020 年 6 月に Amazon Elastic File System (EFS) に対応しました。EFS は NFS でマウントが可能な共有ファイルストレージですが、AWS Lambda で EFS をマウントすることで、より大きなファイルへのバッチ処理や、Lambda 関数間でのファイル共有といった新たなユースケースにも対応できるようになりました。今回のように、動画ファイルのような比較的サイズの大きいファイルを取り扱うユースケースの場合には、Lambda で処理をしつつ、結果を EFS に保存する方法が適していそうです。


全体アーキテクチャ

ここまでを踏まえて、最終的に考えたアーキテクチャはこちらです。 

img_transcribe_youtuber_02

順を追って説明していきます。まず、このソリューションを実現するためには、

  1. 動画に含まれる音声データの書き起こし
  2. 書き起こし結果の分析
  3. 分析結果に基づいた動画のカット
  4. カットした動画の結合

という処理の流れが必要かと思います。このような順序性のある処理を展開していくにあたっては、AWS Step Functions というサービスが最適です。これにより、処理を順番にチェーンしていくようなアーキテクチャを作ることができます。

Step Functions を開始するトリガーとしては、Amazon Simple Storage Service (S3) を利用しています。S3 に動画ファイルを置く (アップロードする) ことで、Step Functions で構成した一連の処理が動作する構成としています。

動画ファイルが S3 へアップロードされると、Step Functions が動き始め、動画ファイルの音声の書き起こしが Transcribe で行われます。書き起こしには少し時間がかかるので、完了まで待機します。書き起こしが完了したら、動画ファイルを共有ファイルストレージである EFS へダウンロードしておき、後の Lambda がアクセスしやすいようにしておきます。

次に、Transcribe で書き起こした結果から、日本語が発話されている区間 (時間帯) を調べ、Amazon Simple Queue Service (SQS) と Amazon DynamoDB へ書き込みます。

SQS へメッセージが送られると、発話区間にもとづき、元の動画を実際にカットする Lambda が同時並列に動作します。カットした動画は EFS へ保存し、処理結果を DynamoDB へ書き込みます。

最後に、カットした動画をひとつの動画に結合し、S3 へアップロードしたら無事に完成です。


試してみる 💪

今回つくったソリューションのソースコードは、こちらからダウンロードできますので、あらかじめダウンロードしておきましょう。


準備

Lambda 関数については Python 3.8 で作成しています。Python 環境が無い方は、こちらなど、ご自身の環境へ Python をインストールしてください。

利用する AWS のサービスはすべて AWS CloudFormation のテンプレートにまとめています。また、Lambda などサーバーレスのサービスについては、ビルドとデプロイをより簡単に行うために、AWS Serverless Application Model (SAM) と、AWS SAM CLI を使っています。

AWS SAM CLI のインストールがお済みでない方は、こちらのガイドを参考に、インストールをお願いいたします。また、AWS Command Line Interface (CLI) についても、インストールガイドおよび設定ガイドを参考に、セットアップをお願いいたします。


デプロイする

SAM CLI がインストールされたら、デプロイまでは簡単です。

template.yaml が配置されているディレクトリに移動し、以下のコマンドを実行し、Lambda 関数にデプロイするアプリケーション パッケージをビルドします。

sam build

成功すると、最後に次のような画面が表示されます。 

img_transcribe_youtuber_01

続いて、AWS の環境へデプロイします。以下のコマンドを実行してください。 

sam deploy --guided

デプロイの初回の実行時には、いくつかのパラメータを入力します。 

img_transcribe_youtuber_03

クリックすると拡大します

各パラメータの詳細は以下のとおりですが、迷った場合は画像のとおりに入力されるとよいかと思います。

  • Stack Name : 生成される CloudFormation のスタック名
  • AWS Region : デプロイ先のリージョン名
  • Parameter NoSoundCutDuration : 何秒間の間が空いていたらカットの対象とするかを入力します。デフォルトでは 0.2 秒以上の間が空いていたらカットされます。
  • Confirm changes before deploy : 生成されるリソースを確認するかどうか指定します。
  • Allow SAM CLI IAM role creation : IAM ロールの生成を許可するかどうかを入力します。今回の CloudFormation テンプレートは IAM ロール生成を含むため、`Y` を入力してください。
  • Save arguments to samconfig.toml : 入力したパラメータをファイルに保存するかどうか指定します。

パラメータを入力し終わると、生成されるリソースの一覧が表示され、デプロイしてよいか尋ねられるので、確認のうえ y を入力します。

img_transcribe_youtuber_04

クリックすると拡大します

デプロイに成功すると、最後にこのような画面が表示されますので、これで作業は完了です。 

img_transcribe_youtuber_05

クリックすると拡大します

InputS3BucketName が元となる mp4 動画を配置する S3 バケット名、StepFunctionsName が Step Functions の名称、OutputS3BucketName が結果の mp4 動画が格納される S3 バケット名になります。


実際に動かす

まずは AWS のコンソールにアクセスし、S3 のコンソールを開きます。S3 のコンソールで、InputS3BucketName に表示されたバケット名を検索し、バケットにアクセスします。 

img_transcribe_youtuber_06

クリックすると拡大します

アップロードボタンをクリックし、音声ファイルの含まれた任意の mp4 動画をアップロードします。 

img_transcribe_youtuber_07

クリックすると拡大します

続いて、Step Functions のコンソールを開きます。StepFunctionsName に表示されていた名称のリソースを開きます。

img_transcribe_youtuber_23

クリックすると拡大します

実行中となっている履歴があるかと思いますので、こちらを開きます。 

img_transcribe_youtuber_08

クリックすると拡大します

開くと、現在実行中のイベント履歴を確認できます。 

img_transcribe_youtuber_09

クリックすると拡大します

しばらくすると、実行状態が「成功」に変わります。動画が長ければ長いほど、実行状態が変わるまでの時間も比例して長くなります。 

img_transcribe_youtuber_22

クリックすると拡大します

S3 のコンソールに戻り、今度は OutputS3BucketName  に表示されていたバケット名を検索して表示しクリックします。

img_transcribe_youtuber_10

クリックすると拡大します

「ダウンロード」をクリックすることで、できあがった動画がダウンロードできます。開いて、無音部分がカットできているか、結果を確認してみるとよいでしょう。 

img_transcribe_youtuber_12

技術解説

ここからは、動作の仕組みが気になる方向けに、今回のソリューションの内容をより深く解説していきます。処理の細かな動作についてはソースコードをご確認いただければと思いますので、要点を絞って取り上げていきたいと思います。 

動画ファイル配置から Step Functions の起動まで

まずは動画ファイルを S3 へ配置し、Step Functions が起動されるまでを見てみます。アーキテクチャのなかでも、赤枠で囲った部分ですね。 

img_transcribe_youtuber_13

クリックすると拡大します

CloudFormation テンプレートでは、以下のように定義されています。 

yaml

StartStateMachineFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: aws_controllers/
      Handler: step_functions.start_state_machine
      Policies:
        - StepFunctionsExecutionPolicy:
            StateMachineName: !Sub "${AWS::StackName}-state-machine"
      Environment:
        Variables:
          STATE_MACHINE_ARN: !Sub "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${AWS::StackName}-state-machine"
      Events:
        CreateObject:
          Type: S3
          Properties:
            Bucket: !Ref S3InputBucket
            Events:
              - s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                - Name: suffix
                  Value: .mp4

Events のあたりを見ていただくと、S3 バケット内に mp4 ファイルが配置されることで起動される関数である、ということが直感的に分かるのではないでしょうか。ここでは、AWS Serverless Application Model (SAM) と呼ばれる、CloudFormation の省略記法が使われており、簡潔に Lambda 関数を定義しています。

また、少し余談になりますが、S3 バケットをトリガーとして Lambda 関数を実行する場合、At Least Once の実行保証が得られるのも嬉しい点と言えます。

この Lambda 関数が Step Functions を起動するところから、処理は始まります。


AWS Step Functions の全体像

見てきたとおり、今回のアーキテクチャの中心には Step Functions が存在し、Lambda 関数を順番に起動していくことで動画のカットを実現しています。実行される Step Functions の全体像を見てみましょう。 

img_transcribe_youtuber_14

クリックすると拡大します

このように、Step Functions では、実行したい内容が State と呼ばれる単位で、数珠つなぎのように順次実行されていきます。

それぞれの実行内容は以下の通りです。

  • StartTranscribeJob: Transcribe を使って動画ファイルの音声を書き起こすジョブを開始する。
  • CheckTranscribe : 書き起こしのジョブが終了するまでには時間がかかるので、ジョブが完了するまで待機する。10 秒ごとにジョブのステータスを確認し、一定時間を過ぎたらタイムアウトとして失敗させる。ジョブが完了したら次に遷移する。
  • DownloadSourceMovie : S3 にある動画ファイルと、Transcribe の書き起こし結果 (JSON 形式のテキストファイル) を EFS に保存する。
  • AnalyzeTranscribeJob : Transcribe の書き起こし結果を元に、日本語が発話されている区間を調べ、区間ごとに SQS と DynamoDB へデータを保存する。SQS にデータが送信されると、別の Lambda 関数が起動され、発話区間に応じて動画のカットを行う。
  • CheckClipping : 動画のカットがすべて完了するまでには時間がかかるので、完了するまで待機する。10 秒ごとに全てのカットが完了したかを確認し、一定時間を過ぎたらタイムアウトとして失敗させる。全てのカットが無事に完了したら次に遷移する。
  • ConcatMovies : カットされた動画を結合し EFS に保存する。
  • UploadMovie : 結合した動画を S3 にアップロードする。

複雑に見えるかもしれないですが、大丈夫です。順を追ってみていきましょう。


音声データの書き起こしと完了チェック

まずは Transcribe が起動され、書き起こしの完了まで待機する部分です。 

img_transcribe_youtuber_15

クリックすると拡大します

Step Functions ではこの部分が該当します。

img_transcribe_youtuber_16

クリックすると拡大します

このなかで注目したい点は、書き起こし完了までの待機です。どのように実現されているかを、ソースコードを抜粋して見てみます。

job_orchestrators/waiter.py 

transcribe = boto3.client("transcribe")

def check_transcribe_status(event, context):
    """
    Transcribe のジョブのステータスを確認し、完了まで待機する関数

    Parameter
    ----------
    - event: Step Functions からの入力
        - job_name: Transcribe の実行ジョブ名
        - iterator:
            - index: 何回目のステータス確認かを表すインデックス
    - context: Lambda 関数の実行コンテキスト

    Return
    ----------
    - iterator:
        - index: 何回目のステータス確認かを表すインデックス
        - is_continue: 待機した後にもう一度ステータス確認を行うか示すフラグ
        - is_timeout: ステータス確認回数の上限を超えたかどうかを示すフラグ

    """

    logger.info({"event": event})
    job_name = event["job_name"]

    try:
        response = transcribe.get_transcription_job(
            TranscriptionJobName=job_name
        )
        logger.info({"transcribe_job_status": response})
    except ClientError as e:
        logger.error({"error": e})
        raise
    transcribe_status = response["TranscriptionJob"]["TranscriptionJobStatus"]

    max_count = 200
    index = event["iterator"]["index"]
    index += 1
    is_timeout = False
    is_continue = True
    if transcribe_status == "COMPLETED":
        is_continue = False
        index = 0
    elif index > max_count or transcribe_status == "FAILED":
        is_timeout = True
        is_continue = False

    iterator = {
        "index": index,
        "is_continue": is_continue,
        "is_timeout": is_timeout,
    }
    return iterator

このように、Transcribe の実行結果を確認しステータスが COMPLETED になっていなければ、index の値をインクリメントしています。その後、Step Functions により 10 秒の wait ののちに、再度この関数を実行します。

index の値が 1 ずつ増加していきますが、max_count である 200 を超えると、タイムアウトとみなして is_timeout のフラグを True として、Step Functions の実行を失敗させています。


動画のダウンロードと発話区間の解析

つづいて、S3 から動画と書き起こし結果をダウンロードし、発話されている時間を解析します。アーキテクチャのなかでもこちらの赤枠の箇所になります。

img_transcribe_youtuber_17

クリックすると拡大します

Step Functions ではこの部分が該当します。 

img_transcribe_youtuber_18

クリックすると拡大します

ここでは、発話区間を分析する関数に着目してみたいと思います。ソースコードは以下のとおりです。 

job_orchestrators/analyzer.py

import json
import os
from decimal import Decimal
import boto3
from aws_lambda_powertools.logging import Logger
from botocore.exceptions import ClientError

TABLE_NAME = os.getenv("DYNAMODB_TABLE_NAME")
QUEUE_NAME = os.getenv("SQS_QUEUE_NAME")
NO_SOUND_DURATION = Decimal(os.getenv("NO_SOUND_DURATION", "0.2"))
DIRECTORY = "/mnt/lambda"

dynamo = boto3.resource("dynamodb")
sqs = boto3.resource("sqs")
table = dynamo.Table(TABLE_NAME)
queue = sqs.get_queue_by_name(QueueName=QUEUE_NAME)
logger = Logger()


def analyze_speech(event, context):
    """
    動画の書き起こし結果から発話区間 (無音でない区間) を分析し、
    SQS と DynamoDB へ区間ごとに結果を送信・保存する関数

    Parameter
    ----------
    - event: Step Functions からの入力
        - job_name: Step Functions の実行名
        - file_name: S3 にアップロードされた動画のファイル名
    - context: Lambda 関数の実行コンテキスト

    Return
    ----------
    - file_count: 生成される動画ファイル数

    """

    logger.info({"event": event})
    job_name = event["job_name"]
    file_name = event["file_name"]

    def clip_speech_activity(movie_start, movie_end, index):
        """
        発話区間のデータを SQS と DynamoDB へ書き込む関数

        Parameter
        ----------
        - movie_start: 発話区間の開始時間
        - movie_end: 発話区間の終了時間
        - index: 何番目の発話区間かを表すインデックス

        Return
        ----------
        - index: 次の発話区間のインデックス

        """

        duration = movie_end - movie_start
        if duration != 0:
            message = {
                "job_name": job_name,
                "index": index,
            }
            record = {
                **message,
                "file_name": file_name,
                "start_time": movie_start,
                "duration": duration,
            }
            try:
                queue.send_message(MessageBody=json.dumps(message))
                table.put_item(Item=record)
            except ClientError as e:
                logger.error({"error": e})
                raise
            index = index + 1
        return index

    transcription_path = f"{DIRECTORY}/{job_name}/input/transcription.json"
    try:
        with open(transcription_path, "r") as file:
            transcription = json.load(file)
    except json.JSONDecodeError as e:
        logger.error({"error": e})
        raise
    transcription_items = transcription["results"]["items"]

    last_movie_start = 0
    last_movie_end = 0
    next_index = 0
    for item in transcription_items:
        if item["type"] != "pronunciation":
            continue
        start_time = Decimal(item["start_time"])
        if start_time - last_movie_end >= NO_SOUND_DURATION:
            next_index = clip_speech_activity(
                last_movie_start, last_movie_end, next_index
            )
            last_movie_start = start_time
        last_movie_end = Decimal(item["end_time"])

    file_count = clip_speech_activity(
        last_movie_start, last_movie_end, next_index
    )
    return file_count

EFS にダウンロードした音声の書き起こし結果をもとにして、NO_SOUND_DURATION (デフォルトでは 0.2 秒) に指定した秒数以上無言の区間が発生した場合、SQS と DynamoDB へ、動画の開始時刻と期間を送信します。

実例とともに見たほうが分かりやすいかと思いますので、冒頭にもあげた Transcribe の結果とともに見てみましょう。

{
  "items":[{
    "start_time":"4.06",
    "end_time":"4.21",
    "alternatives":[{
      "confidence":"0.831",
      "content":"みな"
    }],
    "type":"pronunciation"
  },{
    "start_time":"4.21",
    "end_time":"4.36",
    "alternatives":[{
      "confidence":"0.8416",
      "content":"さん"
    }],
    "type":"pronunciation"
  },{
    "start_time":"4.36",
    "end_time":"4.82",
    "alternatives":[{
      "confidence":"1.0",
      "content":"こんにちは"
    }]
  },{
    "start_time":"5.23",
    "end_time":"5.81",
    "alternatives":[{
      "confidence":"1.0",
      "content":"アマゾンウェブサービス"
    }]
  }]
}

この場合、「みなさんこんにちは」という単語が 4.06 秒 から 4.82 秒 までの 0.76 秒のあいだ、話されていることが分かります。そのため、動画の開始時刻を 4.06 秒、期間を 0.76 秒間、というデータを SQS と DynamoDB へ記録します。

次の動画は 5.23 秒からはじまることになり、結果として 4.82 秒から 5.23 秒までの間がカットされることになります。

こちらでも、書き起こし完了を待機していた関数と同様に、動画のカット完了までを待機させています。10 秒待機する仕組みも同様です。

書き起こしの際は、Transcribe のステータスが COMPLETED になっているかどうかで確認をしましたが、動画のカット完了の判断は DynamoDB のテーブルを見て行っています。どのように判断しているかは次で説明します。


動画のカット

SQS にメッセージが届くと、実際に動画をカットする Lambda 関数が、並列に起動されていきます。アーキテクチャ図ではこちらの赤枠の箇所になります。 

img_transcribe_youtuber_19

クリックすると拡大します

処理が無事に完了したら、DynamoDB のテーブルから該当レコードを削除しています。そのため、全てのカット処理が完了したかどうかは、DynamoDB のテーブルのレコード件数を見ることで判断できるということになります。

実際の動画のカットを行っている箇所のソースコードがこちらです。 

movie_formatters/movie.py 

def clip_speech(event, context):
    """
    SQS でカットの対象となる発話区間を含んだメッセージを受信すると、
    その区間にもとづき、動画のカットを行う関数
    正常に完了した場合、DynamoDB から該当するレコードを削除する

    Parameter
    ----------
    - event: SQS から受信したメッセージ
        - Records: 受信したメッセージの配列
            - job_name: Step Functions の実行名
            - index: 何番目の発話区間かを表すインデックス
    - context: Lambda 関数の実行コンテキスト

    Return
    ----------
    None

    """

    def spawn_clip_process(start_time, duration, input, output):
        """
        動画のカットを行い EFS 上に保存するプロセス (ffmpeg) を立ち上げる関数

        Parameter
        ----------
        - start_time: 発話区間の開始時間
        - duration: 発話区間の長さ
        - input: カットの対象となる動画ファイルのパス
        - output: カットした動画ファイルを配置するパス

        Return
        ----------
        - is_success: プロセスの実行が成功したかどうかを表すフラグ

        """

        cmd = [
            "./ffmpeg",
            "-y",
            "-ss",
            str(start_time),
            "-i",
            input,
            "-t",
            str(duration),
            output,
        ]
        result = subprocess.run(
            cmd,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        stdout = result.stdout.decode("utf8")
        stderr = result.stderr.decode("utf8")
        if len(stdout) != 0:
            logger.info({"ffmpeg_stdout": stdout})
        if len(stderr) != 0:
            logger.info({"ffmpeg_stderr": stderr})

        is_success = True
        if result.returncode != 0:
            is_success = False
        return is_success

    logger.info({"event": event})

    messages = event["Records"]
    for message in messages:
        message_body = json.loads(message["body"])
        job_name = message_body["job_name"]
        index = message_body["index"]

        try:
            record = table.get_item(Key={"job_name": job_name, "index": index})
        except ClientError as e:
            logger.error({"error": e})
        item = record["Item"]
        logger.info({"dynamodb_item": item})

        file_name = item["file_name"]
        input_file = f"{DIRECTORY}/{job_name}/input/{file_name}"
        output_file = f"{DIRECTORY}/{job_name}/clipped/{index}.mp4"
        is_success = spawn_clip_process(
            item["start_time"], item["duration"], input_file, output_file
        )
        if is_success:
            try:
                table.delete_item(Key={"job_name": job_name, "index": index})
            except ClientError as e:
                logger.error({"error": e})
                raise
        else:
            raise MovieClippingError()

    return

動画のカットには ffmpeg を使わせていただきました。ffmpeg を CLI から立ち上げ、発話区間に応じた動画のカットを行っています。カットした動画は EFS へ保存し、処理の最後に DynamoDB からレコードを削除しています。


動画の結合とアップロード

ここまできたらあと少しです。最後に、カットした動画を結合し、S3 へアップロードしたら終了となります。アーキテクチャ図のなかでも以下の箇所ですね。 

img_transcribe_youtuber_20

クリックすると拡大します

Step Functions ではこの部分が該当します。 

img_transcribe_youtuber_21

クリックすると拡大します

動画の結合は、カットと同じく ffmpeg を使っています。ffmpeg では、結合対象となる動画のファイルパスを、以下のようにテキストファイルで指定すると、それらを順番に結合することができます。

file /mnt/lambda/clipped/0.mp4
file /mnt/lambda/clipped/1.mp4
file /mnt/lambda/clipped/2.mp4
file /mnt/lambda/clipped/3.mp4

今回は、カットした動画には上記のように順番にインデックス番号が振られるので、これらをテキストファイルにまとめ、ffmpeg で結合した動画を EFS に保存する方法を取りました。

最後に、結合された EFS 上の動画を S3 へアップロードして、処理は完了となります。


🙇お疲れさまでした ! 🙇

ボリュームの多い内容でしたが、最後まで読んでいただき、ありがとうございました!

今回は実装しませんでしたが、ご紹介した Transcribe では、「えーっと」や「あのー」といったいわゆる「言葉のヒゲ」も書き起こされるので、そのあたりのトリム処理も入れてあげると、よりテンポの良い動画が生成できるのではないかと思いました。

他にも、Transcribe で得られた書き起こしデータから字幕を生成したり、文章の区切りで SE (サウンドエフェクト) を自動挿入するようなサービスができたら素敵だなと思いましたが、今回はこのあたりで筆を置きたいと思います。続きを見たい、という奇特な方がもしいれば、いつか書いてみたいですね・・・。

これを機会に、皆様の動画にも YouTuber 感をちょい足ししてみてはいかがでしょうか。 


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

photo_imamura_yuta

今村 優太 (いまむら ゆうた)
アマゾン ウェブ サービス ジャパン合同会社
Solutions Architect, Prototyping Specialist

国内および外資クラウドベンダーで経験を積み、2018 年より Amazon Web Services Japan へ入社。その後、Prototyping Specialist として、AWS を利用するお客様のプロトタイプ作成協力や PoC 支援を主業務とする。特に好きな技術領域はサーバーレスの分野で、AWSのサーバーレステクノロジーの普及にも力を入れている。ペットの猫の名前はツナマヨ。

さらに最新記事・デベロッパー向けイベントを検索

下記の項目で絞り込む
絞り込みを解除 ≫
1

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する