feat: use VOICEVOX API for TTS
This commit is contained in:
		@@ -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<string> {
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user