Intel Edison から JRuby で Google Calendar のスケジュールを Amazon Polly に喋らせる

 Edison 上で Java の既存ライブラリを Ruby から使ってみたいというケースがあったので、 JRuby を試してみました。前回のスケジュールリマインダーの Amazon Polly へのアクセス部分を JRuby から AWS SDK for Java を使う形に変更してみます。

jruby.org

JRuby インストール

 まずは JRuby をインストールします。 JRuby のサイトから最新のバイナリをダウンロードして展開します。 2017/08/31 時点では Ruby2.3 互換の 9.1.12.0 が最新でした。

# wget https://s3.amazonaws.com/jruby.org/downloads/9.1.12.0/jruby-bin-9.1.12.0.tar.gz
# gunzip jruby-bin-9.1.12.0.tar.gz
# tar xf jruby-bin-9.1.12.0.tar

 展開したバイナリの bin ディレクトリにパスを通すために、 ~/.profile に下記を追記します。 bin ディレクトリのパスは実際の展開先のパスに置き換えてください。

export PATH=$PATH:/path/to/jruby-9.1.12.0/bin

 jruby コマンドにパスが通っていることを確認します。

# jruby -v
jruby 9.1.12.0 (2.3.3) 2017-06-15 33c6439 Java HotSpot(TM) Client VM 25.40-b25 on 1.8.0_40-b25 +jit [linux-i386]

bundler 使用設定

 JRuby から bundler を使うための設定をします。まずは JRuby で bundler をインストールします。 jruby -S の後にコマンドをつなげると JRuby としてのコマンド実行になります。

# jruby -S gem install bundler
Fetching: bundler-1.15.4.gem (100%)
Successfully installed bundler-1.15.4
1 gem installed

 無事にインストールされたので bundle install を実行します。

# jruby -S bundle install --path vendor/bundle

AWS SDK for Java で Amazon Polly へアクセス

 JRuby を使うための環境ができたので、 AWS SDK for Java を使って Amazon Polly へアクセスするコードを用意します。下記にコード全体(notifier_jruby.rb)を掲載します。

require 'bundler/setup'
require 'open3'
require 'digest/md5'

require 'java'
require 'aws-java-sdk-1.11.185.jar'
require 'commons-logging-1.2.jar'
require 'commons-codec-1.9.jar'
require 'httpcore-4.4.6.jar'
require 'httpclient-4.5.3.jar'
require 'jackson-core-2.9.0.jar'
require 'jackson-databind-2.9.0.jar'
require 'jackson-annotations-2.9.0.jar'

java_import 'java.lang.System'
java_import 'java.nio.file.Files'
java_import 'java.nio.file.StandardCopyOption'
java_import 'com.amazonaws.auth.BasicAWSCredentials'
java_import 'com.amazonaws.auth.AWSStaticCredentialsProvider'
java_import 'com.amazonaws.services.polly.AmazonPollyClientBuilder'
java_import 'com.amazonaws.services.polly.model.SynthesizeSpeechRequest'

class Notifier
  VOICE_DIR = '/work/schedule_reminder/voices'

  REGION            = 'us-east-1'
  ACCESS_KEY_ID     = ENV['ACCESS_KEY_ID']
  SECRET_ACCESS_KEY = ENV['SECRET_ACCESS_KEY']
  VOICE_ID_JP       = 'Mizuki'

  def initialize
    aws_credentials = BasicAWSCredentials.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    aws_credentials_provider = AWSStaticCredentialsProvider.new(aws_credentials)
    @client = AmazonPollyClientBuilder.standard.withCredentials(aws_credentials_provider).withRegion(REGION).build
  end

  def speech(text)
    text_hash = Digest::MD5.hexdigest(text)
    target_file = "#{VOICE_DIR}/#{text_hash}"

    synthesize(text, target_file)
    convert(target_file)
    play(target_file)
  end

  def synthesize(text, target_file)
    speech_request = SynthesizeSpeechRequest.new.withText(text).withVoiceId(VOICE_ID_JP).withOutputFormat('mp3')
    speech_result = @client.synthesizeSpeech(speech_request)
    file = java.io.File.new("#{target_file}.mp3")
    Files.copy(speech_result.getAudioStream, file.toPath, StandardCopyOption.valueOf('REPLACE_EXISTING'))
  end

  def convert(target_file)
    Open3.capture3("mpg123 -w #{target_file}.wav #{target_file}.mp3")
  end

  def play(target_file)
    Open3.capture3("aplay #{target_file}.wav")
  end
end

 まずは使用する jar ファイルをダウンロードして ./lib ディレクトリに配置し、 CLASSPATH環境変数にそのディレクトリのパスを設定した上で、読み込むために各 jar ファイルを require します。

require 'java'
require 'aws-java-sdk-1.11.185.jar'
require 'commons-logging-1.2.jar'
require 'commons-codec-1.9.jar'
require 'httpcore-4.4.6.jar'
require 'httpclient-4.5.3.jar'
require 'jackson-core-2.9.0.jar'
require 'jackson-databind-2.9.0.jar'
require 'jackson-annotations-2.9.0.jar'

 そしてJavaのクラスを import するために java_import を使用します。

java_import 'java.lang.System'
java_import 'java.nio.file.Files'
java_import 'java.nio.file.StandardCopyOption'
java_import 'com.amazonaws.auth.BasicAWSCredentials'
java_import 'com.amazonaws.auth.AWSStaticCredentialsProvider'
java_import 'com.amazonaws.services.polly.AmazonPollyClientBuilder'
java_import 'com.amazonaws.services.polly.model.SynthesizeSpeechRequest'

 初期化時に AWS SDK for Java を使って Amazon Polly のクライアントインスタンスを生成します。今回はとりあえずで BasicAWSCredentials で直接 ACCESS_KEY_IDSECRET_ACCESS_KEY を指定していますが、こちらにあるような認証情報プロバイダを使ったやり方の方が望ましいかと思います。

  def initialize
    aws_credentials = BasicAWSCredentials.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    aws_credentials_provider = AWSStaticCredentialsProvider.new(aws_credentials)
    @client = AmazonPollyClientBuilder.standard.withCredentials(aws_credentials_provider).withRegion(REGION).build
  end

 そして Polly へのアクセス部分も AWS SDK for Java を使う形に変更します。 Java の Polly クライアントでは直接保存先ファイルを指定できないので、 AudioStream を取り出して、 java.nio.file.Filescopy メソッドを使用してファイルに保存しています。

 また、 File クラスは Ruby の File クラスとクラス名が重複してしまうので、パッケージ名込みでクラスを指定しています。

  def synthesize(text, target_file)
    speech_request = SynthesizeSpeechRequest.new.withText(text).withVoiceId(VOICE_ID_JP).withOutputFormat('mp3')
    speech_result = @client.synthesizeSpeech(speech_request)
    file = java.io.File.new("#{target_file}.mp3")
    Files.copy(speech_result.getAudioStream, file.toPath, StandardCopyOption.valueOf('REPLACE_EXISTING'))
  end

実行

 今までの notifier.rb の代わりに notifier_jruby.rb を reminder.rb から require するように変更した上で、下記のように CLASSPATH 指定と一緒に実行します。

# CLASSPATH='./lib' jruby -S bundle exec jruby reminder.rb

 もちろん CLASSPATH は export であらかじめ設定しておいてもOKです。

# export CLASSPATH='./lib'
# jruby -S bundle exec jruby reminder.rb

まとめ

 今回はとりあえず試してみるということで、例外処理等は考慮していませんが、Rubyメインで実装しつつ、限定的にJavaのライブラリを使いたいようなケースでは手軽にJavaのクラスを使うことができるので便利そうです。ただ例外処理や依存するライブラリの読み込み、バグ発生時のデバッグのことを考えると、本番で使用するにはなかなかハードルは高そうにも思いました。

 あとは今回はパフォーマンス面は測定しませんでしたが、JVM上での実行となることで同じ Ruby のコードでもパフォーマンスは向上する可能性もあると思いますので、パフォーマンス改善のための選択肢としても可能性はありそうかなと思っています。

 今回のコードはこちらにも公開していますのでよろしければご参照ください。

github.com