Ruby/AWS S3/AWS Lambda/Amazon Transcribeで動画の文字起こしをする

はじめに

RubyとS3とLambdaとTranscribeを使ってS3にアップロードされた動画を自動的に文字起こしするフローを作ってみました。 この記事はその時の手順などをまとめたものになります。

何故やったかというと、妻が「中国語の動画の字幕が欲しい」と言っていたのがきっかけ。 良い感じに動画の文字起こしができるものがないかと探してたところ、S3とLambdaとTranscribeで行けそうな感じだったのでやってみた感じです

やったこと

S3にアップロードされた動画をLambdaで受け取る

まずはS3にアップロードされた動画をLambdaで受け取ることができるようにします。

AWS Lambdaで以下のように関数を作成します。

f:id:gamelinks007:20210515172027p:plain

作成する関数名はs3-upload-hookとします。 また「ランタイム」はRuby 2.7、「デフォルトの実行ロールの変更」は「基本的な Lambda アクセス権限で新しいロールを作成」を選択します。

最後に「関数の作成」をクリックしてLambdaの関数を作成します。

関数の作成後、使用するコードを以下のように変更します。

require 'json'
require 'logger'

def lambda_handler(event:, context:)
    logger = Logger.new($stdout)
    
    logger.info("Hook Event")
    logger.info(event)
    
    record = event["Records"][0]
    s3 = record["s3"]
    region = record["awsRegion"]

    bucket = s3["bucket"]["name"]
    object_key = s3["object"]["key"]
    path = "https://#{bucket}.s3-#{region}.amazonaws.com/#{object_key}"
    job_name = "TranscriptionJobName_#{object_key.sub(/movie\//, "")}"
    output_key = object_key.sub(/movie/, "text").sub(/\.mp4/, "")

    logger.info("S3 info")
    logger.info(bucket)
    logger.info(object_key)
    logger.info(path)
end

コードの内容としては、S3から受け取ったバケットなどの情報を最終的に出力しています。

次に、S3でファイルがアップロードされたイベントをフックできるようにします。

f:id:gamelinks007:20210515172523p:plain

バケット」には動画がアップロードされるバケットを指定します。特定のディレクトリを指定したい場合はプレフィックスmovie/のようにディレクトリ名を指定します

最後に「追加」をクリックしてトリガーを追加します。

ここまでの段階で、S3にアップロードされた動画ファイルをLambdaで受け取ることができるようになります。

Transcribeで文字起こしをするジョブを作成

次に、LambdaからTranscribeへと文字起こしをするジョブの処理を追加します。

Lambdaのコードに以下の部分を追加します。

require 'json'
+ require 'aws-sdk-transcribeservice'
require 'logger'

def lambda_handler(event:, context:)
    logger = Logger.new($stdout)
    
    logger.info("Hook Event")
    logger.info(event)
    
    record = event["Records"][0]
    s3 = record["s3"]
    region = record["awsRegion"]

    bucket = s3["bucket"]["name"]
    object_key = s3["object"]["key"]
    path = "https://#{bucket}.s3-#{region}.amazonaws.com/#{object_key}"
    job_name = "TranscriptionJobName_#{object_key.sub(/movie\//, "")}"
    output_key = object_key.sub(/movie/, "text").sub(/\.mp4/, "")

    logger.info("S3 info")
    logger.info(bucket)
    logger.info(object_key)
    logger.info(path)

+    client = Aws::TranscribeService::Client.new(region: region)
    
+    response = client.start_transcription_job({
+        transcription_job_name: job_name,
+        language_code: "ja-JP",
+        media_format: "mp4",
+        media: {
+            media_file_uri: path
+        },
+        output_bucket_name: bucket,
+        output_key: "#{output_key}.txt"
+    })
+
+    logger.info("TranscriptionJob start #{response.transcription_job.transcription_job_name}")
end

client = Aws::TranscribeService::Client.new(region: region)でTranscribeへ投げるジョブを作成するためのクライアントを生成します。

次に、以下の部分でTranscribeへ投げるジョブを生成しています。

    response = client.start_transcription_job({
        transcription_job_name: job_name,
        language_code: "ja-JP",
        media_format: "mp4",
        media: {
            media_file_uri: path
        },
        output_bucket_name: bucket,
        output_key: "#{output_key}.txt"
    })

transcription_job_nameは実行するジョブの名前を指定しています。ジョブの名前は一意でないといけないため、同じ名前のジョブがあるとエラーになるので注意が必要です。

language_codeでは文字起こしする動画ファイルの言語を指定し、media_formatでは変換元のファイルの種類を指定します。

media_file_uriには変換元の動画ファイルのパスを渡しています。また、output_bucket_nameoutput_keyで文字起こししたテキストをS3のどこに保存するかを指定しています。

ここまでの段階で、動画をS3にアップロードすると自動的に文字起こしされたファイルがS3に作成されるようになります。

おわりに

意外と簡単に実装できたので、このフローをベースに海外の動画の文字起こしサービスとか作っても面白そうかなと思いました。

Rubyのしくみ輪読会を完走した話

はじめに

半年くらい前から少人数で「Rubyのしくみ」の輪読会をやっていて、最近完走しました。 これはその輪読会のまとめ記事です。

ことの発端

半年くらい前に「Rubyのしくみ、輪読会とかないと読む機会無いよね」という話が出て、「じゃあ、輪読会やりましょうか」ということで始まりました。

輪読会の内容

会場

会場は、machida.rbのDiscordチャンネルを、輪読会の感想などを書くツールとしてmachida.rbのesaをお借りしました! この場を借りて感謝申し上げます!

基本的な進め方

基本的に週一回のペースで開催していました。 当日の流れは大体下記の通りですね。

  1. 前回の振り返り(初回だけ開催趣旨など)
  2. 輪読
  3. 当日の会の振り返り

輪読の時は、セクションなどで区切った部分を黙読 ⇒ 感想・疑問点などの共有・議論 という流れで行いました。

全体を通して、黙読部分よりも感想・疑問点の共有などに時間をかけている感じですね。 これはCRubyの実装についての疑問点などが結構出ていたからですね(パーサー周りとかローカル変数のEPとか......)

良かった点

疑問点の解消

疑問に感じたところや分からなかったところを直ぐに議論できたのはかなり良かったですね!

特にパーサーやローカル変数の参照の仕方などは難しい部分でもあったので、そこで出てきた疑問点などを素早く議論して解消できたのは良かったですね。

継続しやすさ

やはり何人かで同じ書籍を読み進めていくので、継続して同じ本を読み進めやすかったですね。 特に書籍の難度が上がるほど輪読会形式で進める恩恵は大きそうです。

サンプルコードの記載

これは途中からなんですが、Wandboxを使って書籍中のサンプルコードをesaに記載するようにしました。 実行結果とセットで記載していたので、書籍中の説明を理解するのに役立っていましたね。

おわりに

輪読会自体は今回が初参加だったんですが、結構良かったので今後も何かしらの形で輪読会をしていきたいですね。

Rubyのエラーメッセージを日本語に差し替えてみた

はじめに

Rubyの初学者の方から「エラーメッセージの読み方がよくわからない」という話を聞き、「Rubyのエラーメッセージを日本語化できないか」試してみた記事です。

作ったもの

Konnyakuというgemを作りました。

github.com

rubygems.org

gem install konnyakuでインストールできます。

このgemではRubyのエラーメッセージを日本語に変換して出力することができます。 例えば、以下のようなコードがあったとします

puts Hoge # => 未定義の定数 Hogeがputsに渡されている

このコードを実行すると以下のようなエラーが表示されます

uninitialized constant Hoge (NameError)

実行結果: [Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

定義されていない定数Hogeを参照しているのでエラーになっているわけですね 慣れていれば問題はないのですが、初学者の方には英語表記であるため読みにくさを感じるようです

そこで、Konnyaku出番です! 先ほどのコードにrequire "konnyaku"を追加すると以下のようにエラーメッセージが日本語で表示されます

require "konnyaku"

p Hoge
#=> 例外:NameError が発生しました
#=> ソースコード: bug.rb の 3 行目にエラーの原因があります
#=> 定義されていない定数 Hoge があります

Konnyakuを使うことで、こんな感じでエラーメッセージを日本語として表示してくれます

やったこと

TracePointを使って、例外が発生したタイミングでエラーメッセージを日本語に強制的に書き換えることをしています。

class Konnyaku
  class << Konnyaku
    def run
        tp = TracePoint.new(:raise) do |tp|
            lineno = tp.lineno
            path   = tp.path
            msg = tp.raised_exception.message
            puts "例外:#{tp.raised_exception.class} が発生しました"
            puts "ソースコード: #{path}#{lineno.to_s} 行目にエラーの原因があります"
            
            msg = translate(msg) 
            puts msg
        end
        tp.enable
    end
  end
end

エラーメッセージなどの変換はKonnyaku.translateで以下のように無理やり置換しています(このへんもう少しきれいにしたい……)

class Konnyaku
  class << Konnyaku
    def translate(msg)
        case msg
            when /uninitialized constant/
                return msg.gsub(/#{$&}/, "定義されていない定数") + " があります"
            when /undefined local variable or method/
                return msg.gsub(/#{$&}/, "定義されていない変数またはメソッド").gsub(/ for /, "").sub(/`|'/, "").gsub(/for/, "") + " にあります"
            when /undefined method/
                return msg.gsub(/#{$&}/, "定義されていないメソッド").gsub(/ for /, "").gsub(/`|'/, "") + " にあります"
            when /uninitialized class variable/
                return msg.gsub(/#{$&}/, "初期化されていないクラス変数").gsub(/ for.+$/, "").gsub(/ in /, "") + " クラスにあります"
        end 
    end
  end
end

今後

とりあえず、対応していないエラーメッセージとかまだまだあるので追加していこうかと思います。 あと、テスト書けてないのでその辺も進めていきたい……

Refinementsで遊んでみた

はじめに

Refinementsで遊んでみた記事です。具体的な内容としてはトップレベルでRefinementsが使えないかなと試してみたとき備忘録です。

Refinementsとは

Rubyでは、後から自由にクラスにメソッドを追加することができる。

class Integer
  def hoge
    puts :hoge
  end
end

42.hoge
# => :hoge

これはこれで便利なんだけど、グローバルに変更されるし、継承とかでも意図しない動きをする可能性があるのであまりいいとは言えない。

そこでRefinementsの出番。

moduel Refine
   refine Integer do
      def hoge
         p :hoge
      end
   end
end

# 42.hoge
# => Error!

using Refine

42.hoge
# => :hoge

Refinementsではusingした移行で再定義したものを使える。 またusingした箇所のファイルスコープに限定されるので影響範囲が小さくて済む。

やりたかったこと

以下のようにトップレベルでrefineを使って特定のクラスにメソッドを生やしたかった(毎回、usingとか書くのが面倒だったので……)

refine Integer do 
   def hoge
      puts :hoge 
   end
end

42.hoge

ただ、このコードはrefineというメソッドが定義されていないのでエラーになる。

やってみた

で、出来そうか試してみたところ以下のコードで動作した。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

refine Integer do 
   public
   def hoge
      p :hoge 
   end
end

42.hoge
# => :hoge

やっていること

Refinementsをよく使われる人は以下のようなコードで無名モジュールを使っているんじゃないかと思う。

using Module.new {
    refine Integer do
        def hoge
            p :hoge
        end
    end
}

42.hoge
# => :hoge

このコードを元にKernelモジュールにrefineメソッドを作成。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

refineメソッドは第一引数に再定義したいクラスなどを受け取り、メソッドの再定義などはブロック引数で渡します。

これにより以下のようにhogeメソッドを追加できます。

refine Integer do 
   public
   def hoge
      puts :hoge 
   end
end

42.hoge
# => :hoge

ただし……

ただ、このコードはusingしていないのに再定義した内容が使用できているんですよね……。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

# ここで usingを使っていないが作成したhogeメソッドが使えている……
refine Integer do 
   public
   def hoge
      p :hoge 
   end
end

42.hoge
# => :hoge

これはRefinementsの仕様通りの動きなのかがはっきりとしていないのでなぜ動いているのやら……。

それと今回のコードではpublicを追加しないとメソッドが呼び出されなかった。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

refine Integer do 
   def hoge
      p :hoge 
   end
end

42.hoge
# => private method `hoge' called for 42:Integer (NoMethodError)

エラーからprivateメソッドとして定義されているらしいことがわかったのでpublicを付けてみたら動いたという……。 この辺も仕様通りの動きなんだろうか……?

おわりに

とりあえず、やりたいことはできた。 これが仕様通りの動きなのかは判断がつかないので、Refinementsの実装とか読んでみようかと思う。

参考

qiita.com

secret-garden.hatenablog.com

secret-garden.hatenablog.com

【Ruby 3.0 Advent Calendar 2020】Dataクラスが廃止された【23日目】

はじめに

Ruby 3.0 Advent Calendar 2020 23日目の記事になります。

昨日は、【Ruby 3.0 Advent Calendar 2020】Ruby 3.0 で WEBrick と SDBM が標準ライブラリから削除される【22日目】です。

secret-garden.hatenablog.com

今日は、Ruby3.0でC拡張などで使われていたDataクラスが削除されたことを紹介します。

Dataクラスって?

DataクラスはObjectクラスからnewallocateundefしたクラスで、C拡張を書く際にnewなどが定義されていると困るケースがあったので使用されていました。 そのためRubyを普通に書く上では特に意識することはありません。

docs.ruby-lang.org

廃止されたことによる影響

基本的にRubyのコードレベルでは大きな影響はありません。 あるとすれば、C拡張を使っているgemなどを作っている人は対応が必要になると思います。

とはいえ、こちらのコミットのようにrb_cObjectを使うように変更すればよさそうなので大きな問題はないかと思います

github.com

あと、大きな問題になりそうなケースとしては古くからメンテナンスされていないC拡張のgemなどは3.0から動かなくなると思います。 なので、かなり古いC拡張のgemを使っている場合は移行先を考えるか自作することを検討するといいかもしれませんね。

【Ruby 3.0 Advent Calendar 2020】C APIのヘッダーが分割された話【16日目】

はじめに

Ruby 3.0 Advent Calendar 2020 16日目の記事になります。

昨日は、【Ruby 3.0 Advent Calendar 2020】ArrayやStringのメソッドの返り値が変更された話【15日目】です。

今日は、Ruby3.0でC APIのヘッダーが分割れたことを紹介します。

C APIとは?

RubyでC(またはC++拡張)を書く際に使用するに使用するAPIです。 このC APIを使ってCレベルでHashやArrayを作成することができるようになります。

// Cレベルで新しい配列 ary を作成
VALUE ary = rb_ary_new()

また、独自クラスの作成などもできたりします。

// Arrayクラスを継承してSubArrayを作成しています。
VALUE rb_cSubArray = rb_define_class("SubArray", rb_cArray)

で、今回の変更は?

簡単に言うと、Ruby側のC APIをメンテナンスしやすくする意図で導入されました。

github.com

またCのマクロを廃止してインライン関数へと変更し、簡単な型チェック(Cレベルでの)が可能になっているようです。

C APIを使うユーザー向けの変更なので、Rubyをそのまま使うユーザー層からすると特に今までのRubyと違う部分はありません。 ただ、いくらかRubyのビルドに時間がかかるかもしれません。

なので、クリスマスにリリースされるRuby3.0のビルド時間は今までより時間がかかるかもしれないので紅茶でも入れながら待つといいかもしれません。

【Ruby 3.0 Advent Calendar 2020】ArrayやStringのメソッドの返り値が変更された話【15日目】

はじめに

Ruby 3.0 Advent Calendar 2020 15日目の記事になります。

昨日は、「Ruby3.0で導入される型定義!TypeprofでBlockやProcを解析してみる」です。

qiita.com

今日は、ArrayStringの返り値が変更された話を紹介します。

これまでの返り値

これまでのRubyではArrayStringを継承したクラスで新しくオブジェクトを作成し、メソッドを実行した際の戻り値が一定のものではありませんでした。

class SubArray < Array
end

sub = SubArray.new([1, 2, 3])
p sub
p sub.class.name
# => SubArray
p sub.uniq.class.name
# => SubArray
p sub.rotate.class.name
# => Array

その為、戻り値のオブジェクトがどのクラスかを確認しなければならなくなり、非常に能率の良くない状況がありました。

実際にArrayの戻り値のクラスが一定ではないことに関してチケットも作成されていました。

bugs.ruby-lang.org

Ruby 3.0ではどうなるのか?

Ruby 3.0ではArrayStringを継承したクラスを使って作成されたに対して継承元(ArrayとかString)のクラスのメソッドを実行すると以下のように継承元のクラスのオブジェクトが返ります。

class SubArray < Array
end

sub = SubArray.new([1, 2, 3])
p sub
p sub.class.name
# => SubArray
p sub.uniq.class.name
# => Array
p sub.rotate.class.name
# => Array

継承を使って作成したSubArrayから一律でArrayが返ってくるので分かりやすくなってますね。 こういうケースだと、SubArrayが返ってくれた方が良かったりすんじゃないかと思ったりもしますが、どちらかにそろっているのは分かり易くてよさそうです

参考

github.com

bugs.ruby-lang.org