(この記事は Node.js の Advent Calendar 2025 の記事【3つ目】にする予定です)
はじめに
この記事は、Node.js で VOICEVOX と BOCCO emo の API を組み合わせて使ってみた話です(それと FFmpeg も組み合わせています)。
以下の動画のように「VOICEVOX で合成した音声で BOCCO emo がしゃべる」ということができます。
実際に試していく
はじめに、全体の処理の概要を書きます。
主な処理の流れは、
「VOICEVOX での音声合成」⇒「WAVファイルを MP3ファイルに変換(FFmpegを利用)」⇒「BOCCO emo の API で MP3ファイルの再生」
となります。
処理の流れに関する補足
少し、処理の流れについて補足します。
VOICEVOX での音声合成と FFmpeg を用いた変換
VOICEVOX で音声合成をすると、その結果は WAVファイルで得られます。そのデータは、直接 BOCCO emo の API では使えません。以下の BOCCO emo の API の公式ドキュメントに書かれているとおり、利用可能なフォーマットが MP3・M4A であるためです。
そのため、FFmpeg を Node.js の処理の中で呼び出して、WAVファイルを MP3ファイルに変換します。その部分の処理については、直近で書いた以下の記事のとおりです。
●Node.js を使った「VOICEVOX での音声合成 + FFmpeg による MP3変換」の処理 - Qiita
https://qiita.com/youtoy/items/3d4eef42ac48c16dc251
BOCCO emo の API を使った音声メッセージ送信
その後は、変換後の MP3ファイルを、BOCCO emo の API を使って BOCCO emo に送ります。前に、以下の記事に書いた「音声メッセージ送信」を使います。
●BOCCO emo の SDK(Node.js版)で API を使った「音声メッセージ送信」を試す - Qiita
https://qiita.com/youtoy/items/77f2545cb6cb9c49b9a8
今回用いたコードなど
実際に試してみます。下準備として、以下のコマンドでパッケージを 1つインストールします。
npm install @ux-xu/emo-platform-api-nodejs
コードの内容
また、今回用いたコードは以下のとおりです。
上で過去に書いた 2つの記事を紹介しましたが、その内容を組み合わせたようなものになります。
import fs from "node:fs";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { EmoApiClient } from "@ux-xu/emo-platform-api-nodejs";
process.loadEnvFile("./development.env");
const BASE_URL = "http://127.0.0.1:50021";
const execAsync = promisify(exec);
async function synthesizeSpeechToMp3(
text,
{ speaker = 3, wavPath = "output.wav", mp3Path = "output.mp3" } = {}
) {
console.log("クエリを作成中");
const queryResponse = await fetch(
`${BASE_URL}/audio_query?text=${encodeURIComponent(
text
)}&speaker=${speaker}`,
{ method: "POST" }
);
if (!queryResponse.ok) {
throw new Error(`クエリ作成失敗: ${queryResponse.status}`);
}
const audioQuery = await queryResponse.json();
console.log("音声合成を実行中");
const synthesisResponse = await fetch(
`${BASE_URL}/synthesis?speaker=${speaker}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(audioQuery),
}
);
if (!synthesisResponse.ok) {
throw new Error(`音声合成失敗: ${synthesisResponse.status}`);
}
const audioBuffer = await synthesisResponse.arrayBuffer();
fs.writeFileSync(wavPath, Buffer.from(audioBuffer));
console.log(`WAV を保存しました: ${wavPath}`);
console.log("mp3 に変換中");
const cmd = `ffmpeg -y -i "${wavPath}" "${mp3Path}"`;
await execAsync(cmd);
console.log(`MP3 を保存しました: ${mp3Path}`);
return mp3Path;
}
async function sendMp3ToBoccoEmo(mp3Path) {
const ACCESS_TOKEN = process.env.EMO_ACCESS_TOKEN;
const REFRESH_TOKEN = process.env.EMO_REFRESH_TOKEN;
const ROOM_UUID = process.env.EMO_ROOM_UUID;
if (!ACCESS_TOKEN || !REFRESH_TOKEN || !ROOM_UUID) {
throw new Error(
"EMO_ACCESS_TOKEN / EMO_REFRESH_TOKEN / EMO_ROOM_UUID が設定されていません"
);
}
const api = new EmoApiClient({
accessToken: ACCESS_TOKEN,
refreshToken: REFRESH_TOKEN,
});
console.log(`Bocco emo に送信中: ${mp3Path}`);
const audio = fs.readFileSync(mp3Path);
const res = await api.postAudioMessage(ROOM_UUID, { audio });
console.log("送信結果:", res);
}
async function main() {
const text = "これはVOICEVOXで作った音声です。";
const wavPath = "bocco_emo.wav";
const mp3Path = "bocco_emo.mp3";
const createdMp3Path = await synthesizeSpeechToMp3(text, {
speaker: 3,
wavPath,
mp3Path,
});
await sendMp3ToBoccoEmo(createdMp3Path);
}
main().catch((err) => {
console.error("エラーが発生しました:", err?.message || err);
console.error("詳細:", err?.response?.status, err?.response?.data);
});
この処理を実行することで、冒頭の動画のような内容を実現できました。
