Edison 上で Java の既存ライブラリを Ruby から使ってみたいというケースがあったので、 JRuby を試してみました。前回のスケジュールリマインダーの Amazon Polly へのアクセス部分を JRuby から AWS SDK for Java を使う形に変更してみます。
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_ID
と SECRET_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.Files
の copy
メソッドを使用してファイルに保存しています。
また、 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 のコードでもパフォーマンスは向上する可能性もあると思いますので、パフォーマンス改善のための選択肢としても可能性はありそうかなと思っています。
今回のコードはこちらにも公開していますのでよろしければご参照ください。