前提条件
- Ruby on Rails 6.x以上
- Dockerインストール済み
- VoiceVoxエンジンがDockerコンテナとして動作中
VoiceVoxのセットアップ
DockerコマンドでVoiceVoxエンジンのセットアップを行います。
docker pull hiroshiba/voicevox_engine:latest
docker run -d -p 127.0.0.1:50021:50021 hiroshiba/voicevox_engine:latest
Railsアプリケーションの設定
Railsプロジェクト側の設定は主に3つの部分に分かれます。サービスクラス設定、コントローラーからのサービスクラスの呼び出し設定、ビューからコントローラアクションの呼び出し設定です。
クラスの設定
app/services/voicevox_service.rb
に以下のようにサービスクラスを作成します。
class VoicevoxService
include HTTParty
# base_uriは、HTTParty gemを使用するクラスメソッドで、このサービスクラスがリクエストを送信する際の基本となるURLを設定
base_uri 'http://localhost:50021'
def self.text_to_speech(text, speaker_id)
# audio_queryのエンドポイントにPOSTリクエストを送信する
# CGI.escapeメソッドは、URLに含めるテキスト文字列を安全にエスケープするために使用される。
query_response = post("/audio_query?text=#{CGI.escape(text)}&speaker=#{speaker_id}")
return nil unless query_response.code == 200
synthesis_response = post("/synthesis?speaker=#{speaker_id}", body: query_response.body, headers: { 'Content-Type' => 'application/json' })
return nil unless synthesis_response.code == 200
synthesis_response.body
end
end
このクラスは、指定されたテキストをVoiceVoxエンジンに送り、合成された音声データを取得します。
コントローラーの設定
app/controllers/blogs_controller.rb
に音声合成のためのアクションを追加します。
class BlogsController < ApplicationController
def read_blog
blog = Blog.find(params[:id])
voice_data = VoicevoxService.text_to_speech(blog.content, 3) # ずんだもんの声
if voice_data
send_data voice_data, type: 'audio/wav', disposition: 'inline'
else
render plain: "音声合成に失敗しました。", status: :internal_server_error
end
end
end
このアクションでは、特定のブログ記事をVoiceVoxで読み上げます。
ビューの設定
ブログ記事を読み上げるためのリンクをビューに追加します。
<%= link_to 'この記事を読み上げる', read_blog_blog_path(@blog), remote: true %>
このリンクは、JavaScriptを介してread_blog
アクションを非同期でトリガーします。
まとめ
以上で完了です。備忘録として残しますが、他の方々に参考になると幸いです。
補足
テストアプリのGitHub
遭遇したエラー
その1: detail":"Not Found
ブラウザでlocalhost:50021
にアクセスした際、{"detail":"Not Found"}
というレスポンスが返されました。これは、APIのエンドポイントが正しく指定されていないことを意味しています。
トラブルシューティング1: VoiceVoxエンジンの動作確認
まずはcURLコマンドを使用して、VoiceVoxエンジンが正常に動作しているかを確認しました。
echo -n "こんにちは、音声合成の世界へようこそ" > text.txt
curl -s \
-X POST \
"localhost:50021/audio_query?speaker=1" \
--get --data-urlencode text@text.txt \
> query.json
curl -s \
-H "Content-Type: application/json" \
-X POST \
-d @query.json \
"localhost:50021/synthesis?speaker=1" \
> audio.wav
上記のcURLコマンドで出力されたaudio.wav
ファイルを再生すると、期待通りに音声が読み上げられました。これにより、VoiceVoxエンジン自体の動作は確認できました。
トラブルシューティング2: VoicevoxServiceクラスの作成
Railsアプリケーション内でサービスクラスVoicevoxService
をapp/services/voicevox_service.rb
に作成しました。
class VoicevoxService
include HTTParty
base_uri 'http://localhost:50021'
def self.text_to_speech(text, speaker_id)
response = post('/audio_query', body: { text: text, speaker: speaker_id }.to_json, headers: { 'Content-Type' => 'application/json' })
return nil unless response.success?
audio_response = post('/synthesis', body: { query: response.parsed_response }.to_json, headers: { 'Content-Type' => 'application/json' })
return nil unless audio_response.success?
audio_response.body
end
end
その2: audio_queryエラー
音声読み上げのリンクを踏むと、「音声合成に失敗しました」とブラウザに表示されました。
トラブルシューティング1: パラメータの確認・エラー箇所の特定
コンソールで確認したところ、VoicevoxService.text_to_speech
メソッドからnil
が返され、エラーメッセージには"field required"
が含まれていました。原因は、テキストとスピーカーIDが正しくリクエストに含まれていなかったことです。
rb(main):004> test_text = "こんにちは、これはテストです。"
irb(main):005> voice_data = VoicevoxService.text_to_speech(test_text)
=> nil
トラブルシューティング2: エラーハンドリングによるエラー箇所の特定
VoicevoxService.rbに以下のようなエラーハンドリングを追加しました。
class VoicevoxService
include HTTParty
base_uri 'http://localhost:50021'
def self.text_to_speech(text, speaker_id)
query_response = post('/audio_query', body: { text: text, speaker: speaker_id }.to_json, headers: { 'Content-Type' => 'application/json' })
# エラーハンドリングを追加
if query_response.code != 200
Rails.logger.error "VOICEVOX audio_query error: #{query_response}"
return nil
end
synthesis_response = post('/synthesis', body: query_response.body, headers: { 'Content-Type' => 'application/json' })
# エラーハンドリングを追加
if synthesis_response.code != 200
Rails.logger.error "VOICEVOX synthesis error: #{synthesis_response}"
return nil
end
synthesis_response.body
end
end
そうすると、以下のエラーログが得られました。エラーはaudio_queryエンドポイントへのリクエストが失敗していることが原因だと判明しました。
VOICEVOX audio_query error: {"detail":[{"loc":["query","text"],"msg":"field required","type":"value_error.missing"},{"loc":["query","speaker"],"msg":"field required","type":"value_error.missing"}]}
トラブルシューティング3: VoicevoxServiceの修正
class VoicevoxService
include HTTParty
base_uri 'http://localhost:50021'
def self.text_to_speech(text, speaker_id)
query_response = post('/audio_query', body: { text: text, speaker: speaker_id }.to_json, headers: { 'Content-Type' => 'application/json' })
return nil unless query_response.code == 200
synthesis_response = post('/synthesis', body: query_response.body, headers: { 'Content-Type' => 'application/json' })
return nil unless synthesis_response.code == 200
synthesis_response.body
end
end
テキストをURLエンコードし、クエリパラメータとしてリクエストに含めるよう修正しました。
class VoicevoxService
include HTTParty
base_uri 'http://localhost:50021'
def self.text_to_speech(text, speaker_id)
query_response = post("/audio_query?text=#{CGI.escape(text)}&speaker=#{speaker_id}")
return nil unless query_response.code == 200
synthesis_response = post('/synthesis', body: query_response.body, headers: { 'Content-Type' => 'application/json' })
return nil unless synthesis_response.code == 200
synthesis_response.body
end
end
その3: synthesisリクエストエラー
/synthesis
エンドポイントへのリクエストで、必要なspeaker
フィールドが欠けているエラーが生じました。
トラブルシューティング: speaker
情報をクエリパラメータに追加
synthesis_response = post("/synthesis?speaker=#{speaker_id}", body: query_response.body, headers: { 'Content-Type' => 'application/json' })