From 7193dd8bb710659461cec2b20b3d11b81ab59314 Mon Sep 17 00:00:00 2001 From: "Satsuki Akiba (aider)" Date: Wed, 4 Jun 2025 11:26:55 +0900 Subject: [PATCH] feat: use VOICEVOX API for TTS --- services/tts.ts | 70 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/services/tts.ts b/services/tts.ts index 1fdd0ee..d7d53c2 100644 --- a/services/tts.ts +++ b/services/tts.ts @@ -1,37 +1,69 @@ import fs from "fs"; import path from "path"; -import { PollyClient, SynthesizeSpeechCommand } from "@aws-sdk/client-polly"; +import fetch from "node-fetch"; -const polly = new PollyClient({ region: "ap-northeast-1" }); +// VOICEVOX APIの設定 +const VOICEVOX_HOST = "http://localhost:50021"; + +interface VoiceStyle { + speakerId: number; + styleId: number; +} + +// 仮の声設定(例: サイドM = 3) +const defaultVoiceStyle: VoiceStyle = { + speakerId: 3, + styleId: 2, +}; export async function generateTTS( itemId: string, scriptText: string, ): Promise { - const params = { - OutputFormat: "mp3", - Text: scriptText, - VoiceId: "Mizuki", - LanguageCode: "ja-JP", - }; - const command = new SynthesizeSpeechCommand(params); - const response = await polly.send(command); + // 音声合成クエリの生成 + const queryResponse = await fetch(`${VOICEVOX_HOST}/audio_query`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: scriptText, + speaker: defaultVoiceStyle.speakerId, + }), + }); - if (!response.AudioStream) { - throw new Error("TTSのAudioStreamが空です"); + if (!queryResponse.ok) { + throw new Error("VOICEVOX 音声合成クエリ生成に失敗しました"); } + const audioQuery = await queryResponse.json(); + + // 音声合成 + const audioResponse = await fetch(`${VOICEVOX_HOST}/synthesis`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + audio_query: audioQuery, + speaker: defaultVoiceStyle.speakerId, + }), + }); + + if (!audioResponse.ok) { + throw new Error("VOICEVOX 音声合成に失敗しました"); + } + + const audioBuffer = await audioResponse.buffer(); + + // 出力ディレクトリの準備 const outputDir = path.join(__dirname, "../public/podcast_audio"); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } - const filePath = path.resolve(outputDir, `${itemId}.mp3`); - const chunks: Uint8Array[] = []; - for await (const chunk of response.AudioStream as any) { - chunks.push(chunk); - } - const buffer = Buffer.concat(chunks); - fs.writeFileSync(filePath, buffer); + const filePath = path.resolve(outputDir, `${itemId}.mp3`); + fs.writeFileSync(filePath, audioBuffer); + return filePath; }