Azure TextToSpeech APIでストリーム処理する - Node.js

AzureのSpeech ServiceでTTS（Text to speech）のAPIを使用して、受け取ったデータを逐次的にStreamで処理をする方法が判明したので紹介します。~~（なぜ、こんな書き方をするのかというと、MSが詳細なドキュメントを公開していないからです。MS仕事しろ！）~~

# 前提

AzureのSpeech Serviceを使える環境を用意してください。
Speech Serviceで合成音声(Speech Synthesis)の始め方はこちらをご覧ください。（C＃ですが・・・）

https://qiita.com/1mono2/items/aa66bf485ede8ec94152

# 結論

`SpeechSynthesizer.speakTextAsync`の引数に`PushAudioOutputStreamCallback`を継承したクラスを用意します。また、`SpeechSynthesizer`の引数には`AudioConfig`を設定してはいけません。

詳細は後からします。

### コード全体

```jsx
import * as MsTtsSdk from "microsoft-cognitiveservices-speech-sdk";

async TTS(text){
		// APIの設定。毎回行う必要はありません。使いまわせます。
    //var audioFileName = "temp_tts_" + uuidv4() + '.wav';;
    const speechConfig = MsTtsSdk.SpeechConfig.fromSubscription(process.env.SPEECH_KEY, process.env.SPEECH_REGION);
    //const audioConfig = MsTtsSdk.AudioConfig.fromAudioFileOutput(audioFileName);
    
		// 言語やスピーカーの声、アウトプットのフォーマットの設定
    speechConfig.speechSynthesisLanguage = "ja-JP";
    speechConfig.speechSynthesisVoiceName = "ja-JP-NanamiNeural"; 
    speechConfig.speechSynthesisOutputFormat = MsTtsSdk.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoMULaw;

    // speech synthesizerクラスのインスタンス化
    var synthesizer = new MsTtsSdk.SpeechSynthesizer(speechConfig);
		// Streamしたデータを処理するコールバッククラスのインスタンス化
    let callback = new SamplePushAudioOutputStreamCallback(this.array);
    
    await synthesizer.speakTextAsync(
			text,
      function (result) {
	      if (result.reason === MsTtsSdk.ResultReason.SynthesizingAudioCompleted) {
	      console.log("synthesis finished.");
	      } else {
	      console.error("Speech synthesis canceled, " + result.errorDetails +
	        "\nDid you set the speech resource key and region values?");
	      }
      synthesizer.close();
      synthesizer = null;
    },
      function (err) {
      console.trace("err - " + err);
      synthesizer.close();
      synthesizer = null;
    }, 
		callback);
  }

class SamplePushAudioOutputStreamCallback extends MsTtsSdk.PushAudioOutputStreamCallback{
  constructor(array) {
    super();
    this.array = array;
  }

  write(data){
      // ここでストリーム化したデータを処理
	    console.log(`Received ${data.byteLength} bytes of audio data.`);

	    // encode to base64
	    let buffer = Buffer.from(data);
	    let base64 = buffer.toString('base64');
	    this.array.add(base64);
  }
  close(){
      console.log('Stream closed.');
  }
}
```

# 解説
### APIの設定
今回は、`SPEECH_KEY`と`SPEECH_REGION`でAPIをConfigを設定します。

スピーカの声の種類は日本語だと、複数あります。[公式リファレンス](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts)を参照してください

```jsx
//var audioFileName = "temp_tts_" + uuidv4() + '.wav';;
const speechConfig = MsTtsSdk.SpeechConfig.fromSubscription(process.env.SPEECH_KEY, process.env.SPEECH_REGION);
//const audioConfig = MsTtsSdk.AudioConfig.fromAudioFileOutput(audioFileName);

// 言語やスピーカーの声、アウトプットのフォーマットの設定
speechConfig.speechSynthesisLanguage = "ja-JP";
speechConfig.speechSynthesisVoiceName = "ja-JP-NanamiNeural"; 
speechConfig.speechSynthesisOutputFormat = MsTtsSdk.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoMULaw;
```

### クラスのインスタンス化
`SpeechSynthesizer`クラスと`PushAudioOutputStreamCallback`を継承したクラスをインスタンス化します。

ここでファイルで保存する時は、ファイルの名前を指定した`AudioConfig`クラスを`SpeechSynthesizer`の引数に含めますが、ストリームで処理をする場合は含めてはいけません。

```jsx
// speech synthesizerクラスのインスタンス化
var synthesizer = new MsTtsSdk.SpeechSynthesizer(speechConfig);
// Streamしたデータを処理するコールバッククラスのインスタンス化
let callback = new SamplePushAudioOutputStreamCallback(this.array);
```

PushAudioOutputStreamCallbackを継承したクラス。

write()はArrayBufferクラスを引数にとり、音声データのバイナリーの配列をここで受け取ることができます。今回は、base64に変換して、用意していた配列に追加する操作をしました。

close()はストリームが終わった時に呼び出されます。
```jsx
class SamplePushAudioOutputStreamCallback extends MsTtsSdk.PushAudioOutputStreamCallback{
  constructor(array) {
    super();
    this.array = array;
  }

  write(data){
      // ここでストリーム化したデータを処理
	    console.log(`Received ${data.byteLength} bytes of audio data.`);

	    // encode to base64
	    let buffer = Buffer.from(data);
	    let base64 = buffer.toString('base64');
	    this.array.add(base64);
  }
  close(){
      console.log('Stream closed.');
  }
}
```

PushAudioOutputStreamCallbackはwrite()とclose()を必要とするアブストラクトクラスです。

```tsx
export declare abstract class PushAudioOutputStreamCallback {
    /**
     * Writes audio data into the data buffer.
     * @member PushAudioOutputStreamCallback.prototype.write
     * @function
     * @public
     * @param {ArrayBuffer} dataBuffer - The byte array that stores the audio data to write.
     */
    abstract write(dataBuffer: ArrayBuffer): void;
    /**
     * Closes the audio output stream.
     * @member PushAudioOutputStreamCallback.prototype.close
     * @function
     * @public
     */
    abstract close(): void;
}
```

### コールバックを引数に

SpeechSynthesizer.SpeechSynthesizerは以下のような引数をとります。

第一：合成音声にするテキスト

第二：API通信成功時に呼び出される。ただ、何かしらの理由でエラーとなる可能性があるので、上のコードのようにエラーハンドリングが必要

第三：API通信がそもそもできなかった時に呼び出される。

第四：PushAudioOutputStreamCallbackクラスを継承したクラスのインスタンス

```jsx

await synthesizer.speakTextAsync(text, SuccessCallback, ErrorCallback, StreamCallback);

```

ライブラリによれば、`AudioOutputStream`  `PathLike`もいけそうです。

```jsx
/**
 * Executes speech synthesis on plain text.
 * The task returns the synthesis result.
 * @member SpeechSynthesizer.prototype.speakTextAsync
 * @function
 * @public
 * @param text - Text to be synthesized.
 * @param cb - Callback that received the SpeechSynthesisResult.
 * @param err - Callback invoked in case of an error.
 * @param stream - AudioOutputStream to receive the synthesized audio.
 */
speakTextAsync(text: string, cb?: (e: SpeechSynthesisResult) => void, err?: (e: string) => void, stream?: AudioOutputStream | PushAudioOutputStreamCallback | PathLike): void;
```



# リファレンス

公式リファレンス

https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/index-text-to-speech

公式Github

https://github.com/Azure-Samples/cognitive-services-speech-sdk/blob/master/samples/js/node/synthesis.js

`AudioOutputStream`を用いた方法の記事ではうまくいかず

https://blog.williamchong.cloud/code/2023/10/14/convert-azure-text-to-speech-to-nodejs-stream.html


:::note info
弊社Passinate Geniusでは一緒に働く仲間を募集しています！興味をお持ちいただける方は、[ホームページ](https://passionategenius.com/)まで！
:::