0
0

RailsアプリでVoiceVoxを使った音声読み上げ機能の実装

Last updated at Posted at 2024-02-29

前提条件

  • 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アプリケーション内でサービスクラスVoicevoxServiceapp/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に以下のようなエラーハンドリングを追加しました。

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の修正

修正前.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' })
    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エンコードし、クエリパラメータとしてリクエストに含めるよう修正しました。

修正後.rb
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' })
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0