3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LiveViewでVOICEVOXを楽に使う手段を作ってみた

Last updated at Posted at 2025-12-03

VOICEVOXの結果をブラウザで再生をしたいという希望があります
なので作りました
Macで検証してますが理論上他のOSでもいけます

検証環境

  • M4 Mac mini 16GB
  • macOS Tahoe 26.1
  • Elixir 1.17.3-otp-27
  • Erlang 27.1.2
  • VOICEVOX Ver 0.25.0

image.png

実行イメージ

環境構築

前提 VOICEVOXを使ったことある人

参考までに軽く書きます

ここからダウンロードしてください
Macの場合いは権限問題でハマります

ここをヒントに自力で解決してください

ぶっちゃけ他のOSの方がハマらない
Ubuntuはあっさりでした

プログラムを書く

前提 mix phx.newでプロジェクトを作成ずみ

ルータを書き換える

lib/voicex_web/router.ex
defmodule VoicexWeb.Router do
  use VoicexWeb, :router
# 省略 #
  scope "/", VoicexWeb do
    pipe_through :browser

-   get "/", PageController, :home
+   live "/", Voice, :index
  end
# 省略 #
end

hookを作成

この部分がVOICEVOXの通信制御コアです
jsで作成します

assets/js/hooks/voicex.js

// VOICEVOX EngineのURL
const VOICEVOX_URL = "http://localhost:50021"; 

Voicex = {
    // ライフサイクルコールバック (要素がDOMに追加され、LiveViewと接続された時に実行)
    mounted() {
        // Elixirサーバー側から送信されるイベントをリッスン
        // イベント名: "synthesize_and_play"
        // ペイロード: { text: "...", speaker_id: N }
        this.handleEvent("synthesize_and_play", ({ text, speaker_id }) => {
            this.speakText(text, speaker_id);
        });
    },

    // --- 1. VOICEVOX API通信関数 (Hook内部関数として定義) ---

    /**
     * 1. VOICEVOX APIを使って音声合成クエリを取得します (audio_query)。
     */
    async fetchAudioQuery(text, speakerId) {
        const queryParams = new URLSearchParams({ text: text, speaker: speakerId });
        const queryUrl = `${VOICEVOX_URL}/audio_query?${queryParams}`;

        const queryResponse = await fetch(queryUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' }
        });

        if (!queryResponse.ok) {
            throw new Error(`audio_query failed with status ${queryResponse.status}`);
        }
        return await queryResponse.json();
    },

    /**
     * 2. VOICEVOX APIを使って音声合成を実行し、WAV形式のBlobを取得します (synthesis)。
     */
    async fetchSynthesis(audioQuery, speakerId) {
        const synthesisParams = new URLSearchParams({ speaker: speakerId });
        const synthesisUrl = `${VOICEVOX_URL}/synthesis?${synthesisParams}`;

        const synthesisResponse = await fetch(synthesisUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(audioQuery)
        });

        if (!synthesisResponse.ok) {
            throw new Error(`synthesis failed with status ${synthesisResponse.status}`);
        }
        return await synthesisResponse.blob();
    },

    // --- 2. コアロジック関数 (Hook内部関数として定義) ---

    /**
     * VOICEVOX APIを使って音声データ(Blob)を取得する純粋なロジック関数。
     */
    async synthesizeTextToBlob(text, speakerId) {
        const trimmedText = text.trim();
        if (!trimmedText) {
            throw new Error("Text input is empty."); 
        }

        // 1. クエリ取得 (Hook内部関数を呼び出し)
        const audioQuery = await this.fetchAudioQuery(trimmedText, speakerId);
        
        // 2. 音声合成 (Hook内部関数を呼び出し)
        const wavBlob = await this.fetchSynthesis(audioQuery, speakerId);

        return wavBlob;
    },

    // --- 3. メインアプリケーション関数 (Hookの主要ロジック) ---

    /**
     * 音声合成と再生を実行するメイン関数。
     */
    async speakText(text, speakerId) {
        try {
            // 1. コアロジックを呼び出し、Blobを取得 (Hook内部関数を呼び出し)
            const wavBlob = await this.synthesizeTextToBlob(text, speakerId);
            
            // 2. JavaScript側で <audio> 要素を生成
            const audioPlayer = new Audio();
            
            // 3. 再生ロジック
            const audioUrl = URL.createObjectURL(wavBlob);
            audioPlayer.src = audioUrl;
            
            // 4. 再生開始
            await audioPlayer.play();

            // 5. 再生終了後のクリーンアップ
            audioPlayer.onended = () => {
                URL.revokeObjectURL(audioUrl);
            };
            audioPlayer.onerror = () => {
                 URL.revokeObjectURL(audioUrl);
            };

        } catch (error) {
            console.error("致命的なエラーが発生しました:", error.message, error);
            
            // エラーロギング
            if (error.message.includes("Text input is empty")) {
                console.error("エラー: テキストが入力されていません。");
            } else if (error.name === "NotAllowedError") {
                console.warn("警告: 再生がブラウザによってブロックされました。");
            } else {
                console.error(`VOICEVOX Engine 接続エラー: ポート (${VOICEVOX_URL}) を確認してください。`);
            }
        } 
    }
};

export default Voicex

上記のファイルを追加します

assets/js/hooks/index.js
import Voicex from "./voicex.js";

export const Hooks = {
  Voicex: Voicex,
};

hookを登録します

assets/js/app.js
// 省略 //
+ import { Hooks } from "./hooks";

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
+ hooks: Hooks,
  params: {_csrf_token: csrfToken}
})

// 省略 //

使う側

lib/voicex_web/live/voice.ex
# my_live_view.ex

defmodule VoicexWeb.Voice do
  use VoicexWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, text: "")}
  end

  def handle_event("update_text", %{"text" => new_text}, socket) do
    {:noreply, assign(socket, text: new_text)}
  end

  def handle_event("speak", %{"text" => text, "speaker-id" => speaker_id}, socket) do
    {:noreply,
     push_event(socket, "synthesize_and_play", %{
       "text" => text,
       "speaker_id" => String.to_integer(speaker_id)
     })}
  end
end

解説

しゃべらせるには
push_event(socket, "synthesize_and_play" を呼びます

     push_event(socket, "synthesize_and_play", %{
       "text" => "話す文章",
       "speaker_id" => 1 #キャラクターの番号1の場合はずんだもん    
       })
lib/voicex_web/live/voice.html.heex
<div id="voicevox-player" phx-hook="Voicex" class="flex flex-col">
  <label for="text_input">話すテキスト</label>
  <form>
    <textarea id="text_input" name="text" phx-change="update_text" class="w-[400px]"></textarea>
  </form>
  <button
    class="bg-b text-brand rounded-full px-2 font-medium leading-6 w-[400px] bg-orange-200 hover:bg-orange-300"
    phx-click="speak"
    phx-value-text={@text}
    phx-value-speaker-id="1"
  >
    話す
  </button>
</div>

ポイント

<div id="voicevox-player" phx-hook="Voicex" class="flex flex-col">

のphx-hook="Voicex"
を書くとhookが使えます
結果jsの部分と連携します

これで使えるようになりました

ソース

ここら必要なソースを取得して使ってください

余談

このソースのほとんどはAIを使って書きました

  • htmlのみでAIに作ってもらう
  • 僕が動作確認をする
  • jsとhtmlをAIで分割する
  • jsのソースをAIを使ってリファクタリングする
    • 不要なソースを消してもらう
    • 意味が通じる単位に分割してもらう
    • 僕が動作確認をする
  • jsをAIを使ってhook化する
  • 動かない部分は僕が修正する

こんな感じで作りました
僕はhookは使ったことがあった&VOICEVOXを使ったとこがあるので
完成の想像がつくのでAIにコードを書いてもらいました

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?