概要

デジタルヒューマンで独自の音声合成(BYO TTS)を利用したいお客様には、独自のカスタムソリューションによって実現できる会話のインテグレーションと同様に、簡単なTTSオーケストレーションサービスを通じて接続がサポートされます。

⚠️
BYO TTSを使用した場合、SynAnimを使用したアクションやカメラ制御等のタイミングと同期できず、利用できない場合があります。詳しくは担当までお問い合わせください。
 

会話の統合と同様に、デジタルヒューマン プラットフォームからの外部リクエストを許可するために、顧客がホストするエンドポイントを作成する必要があります。

デジタルヒューマンプラットフォームは以下のJSONペイロードをお客様のBYO TTSサービスにPOSTします。

{
  "apiKey":"<api-key>",
  "preset":"<preset/voice>",
  "text":"<text to speak>"
}

オーディオの返却

BYO(Bring Your Own)統合APIは、16kHzのシングルチャンネルの生(ヘッダーなし)PCMオーディオを期待しています。2XXステータスコードは成功として扱われ、生成されたレスポンスボディはオーディオとして扱われ、デジタルヒューマンに転送され、ユーザーに再生されます。サンプルは16ビットのリニアエンコードされた符号付き整数(リトルエンディアンバイト順序)として返す必要があります。コンテンツはapplication/octet-streamタイプとして返すことをお勧めします。要約すると、

  • APIは200 OKを返します
  • 16kHzオーディオ
  • モノラル
  • サンプルはリトルエンディアンの16ビット符号付き整数
  • 生PCM、すなわちWAVヘッダーやエンコード、圧縮なし
  • application/octet-stream

フィードバックの返却

エラーは非2XXステータスコードを設定することで返すことができます。これらのエラーコードはカウントされますが、執筆時点ではエラーボディは捕捉されていません。非2XXステータスコードのレスポンスはユーザーに再生されません。

サンプル実装

以下のリストは、Google Cloud TTS APIを呼び出すBYO TTSアプリケーションを実装しています。

以下のPythonサンプルを使用する場合、google-cloud-texttospeechをインストールする必要があります。Googleアプリケーションクレデンシャルの取得方法についてはGoogleのドキュメントを参照してください。サンプルの最初の行では、必要な依存関係をインストールするためにpipを使用しています。

以下のNodeJSサンプルを使用する場合、Microsoft Azure Cognitive Services APIの有効なサブスクリプション、およびコードに記載されている他の環境変数に対する値が必要です。NodeJSコードサンプルには2つの別々のファイルがあり、新しいExpressJSアプリを作成し、オーケストレーションハンドラサンプルを使ってルートを定義し、Microsoft Services Handlerを使ってMicrosoftのCognitive Speechサービスへのインターフェースを定義します。

このドキュメントを読んでいるのであれば、カスタムサービスを作成・ホストすることに慣れていることを前提としています。有償サポートが必要な場合は、お問い合わせください。

python

$ pip3 install google-cloud-texttospeech
import http.server
import socketserver
import json
import os
from google.cloud import texttospeech

os.environ["GOOGLE_APPLICATION_CREDENTIALS"]="./GoogleAuth.json" # this file must exist if you are using Google's text-to-speech service. 

class Handler(http.server.SimpleHTTPRequestHandler):

    def do_GET(self):
        # This is a health API, and doesn't render text to speech
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        self.wfile.write("{\"status\": \"ok\"}".encode('utf-8'))
        return

    def do_POST(self):
        # the actual tts render api
        content_len = int(self.headers.get('Content-Length'))
        post_body = self.rfile.read(content_len).decode('utf-8')

        # body contains api key and preset, but we are not using it
        print('Body: ' + post_body)
        body_json = json.loads(post_body)
        text = body_json['text']
        print('text: ' + text)

        client = texttospeech.TextToSpeechClient()
        synthesis_input = texttospeech.SynthesisInput(text=text)
        
        voice = texttospeech.VoiceSelectionParams(
            language_code='en-US',
            name='en-US-Wavenet-C',
            ssml_gender=texttospeech.SsmlVoiceGender.FEMALE)

        # note sample rate 16K, and audio encoding 16 bit linear
        audio_config = texttospeech.AudioConfig(
            sample_rate_hertz=16000,
            audio_encoding=texttospeech.AudioEncoding.LINEAR16)

        response = client.synthesize_speech(
            input=synthesis_input, voice=voice, audio_config=audio_config
        )

        # Construct a server response. Note the status code 200 and content type
        self.send_response(200)
        self.send_header('Content-type', 'application/octet-stream')
        self.end_headers()

        self.wfile.write(response.audio_content)
        return

print('Server listening on port 3130...')
httpd = socketserver.TCPServer(('0.0.0.0', 3130), Handler)
httpd.serve_forever

node.js

// Orchestration Handler

var express = require('express');
var router = express.Router();
const sdk = require("microsoft-cognitiveservices-speech-sdk");
const {
  textToSpeech,
  textToSpeechSsml
} = require('./azure-cognitiveservices-speech');

const USE_SSML = process.env.USE_SSML;

router.get('/', function (req, res, next) {
  res.send('respond with a resource');
});

router.post('/', async function (req, res, next) {
  let audioStream
  console.log(req.body.text)
  if (USE_SSML === 'true') {
    audioStream = await textToSpeechSsml(req.body.text);
  } else {
    audioStream = await textToSpeech(req.body.text);
  }

  res.set({
    'Content-Type': 'application/octet-stream',
    'Transfer-Encoding': 'chunked'
  });
  audioStream.pipe(res);
})

module.exports = router;



// Microsoft Services Handler
// azure-cognitiveservices-speech.js

const sdk = require('microsoft-cognitiveservices-speech-sdk');
const {
    Buffer
} = require('buffer');
const {
    PassThrough
} = require('stream');
const { OutputFormat } = require('microsoft-cognitiveservices-speech-sdk');

const AZURE_API_KEY = process.env.AZURE_API_KEY;
const AZURE_REGION = process.env.AZURE_REGION;
const AZURE_SPEAKING_STYLE = process.env.AZURE_SPEAKING_STYLE;
const AZURE_OUTPUT_FORMAT = process.env.AZURE_OUTPUT_FORMAT;
const AZURE_VOICE = process.env.AZURE_VOICE;
const PROSODY_SPEED = process.env.PROSODY_SPEED;
const PROSODY_PITCH = process.env.PROSODY_PITCH;
const USE_SPEAKING_STYLE = process.env.USE_SPEAKING_STYLE;
const NLP_OUTPUTS_SSML = process.env.NLP_OUTPUTS_SSML;

/**
 * Node.js server code to convert text to speech
 * @returns stream
 * @param {*} key your resource key
 * @param {*} region your resource region
 * @param {*} text text to convert to audio/speech <== WE ONLY USE TEXT
 * @param {*} filename optional - best for long text - temp file for converted speech/audio
 */
const textToSpeech = async (text) => {

    // convert callback function to promise
    return new Promise((resolve, reject) => {

        const speechConfig = sdk.SpeechConfig.fromSubscription(AZURE_API_KEY, AZURE_REGION);
        speechConfig.speechSynthesisOutputFormat = 14; // Raw16Khz16BitMonoPcm
        speechConfig.speechSynthesisVoiceName = AZURE_VOICE

        let audioConfig = null;

        const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);

        synthesizer.speakTextAsync(
            text = stripUneeqTags(text),
            result => {

                const {
                    audioData
                } = result;

                synthesizer.close();

                const bufferStream = new PassThrough();
                bufferStream.end(Buffer.from(audioData));
                resolve(bufferStream);
            },
            error => {
                synthesizer.close();
                reject(error);
            }
        );
    });
};

const textToSpeechSsml = async (text) => {

    return new Promise((resolve, reject) => {

        const speechConfig = sdk.SpeechConfig.fromSubscription(AZURE_API_KEY, AZURE_REGION);
        speechConfig.speechSynthesisOutputFormat = 14; // Raw16Khz16BitMonoPcm
        speechConfig.speechSynthesisVoiceName = AZURE_VOICE;

        let audioConfig = null;

        // console.log(speechConfig);

        const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);

        let ssmlText = "";

        if (NLP_OUTPUTS_SSML === "true") {
            ssmlText = stripUneeqTags(text)
        } else {
            ssmlText = '<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US">'
            ssmlText += `<voice name="${AZURE_VOICE}">`
            if (PROSODY_SPEED.length > 0) {
                ssmlText += `<prosody rate="${PROSODY_SPEED}" pitch="${PROSODY_PITCH}">`
            }
            if (USE_SPEAKING_STYLE === 'true') {
                ssmlText += `<mstts:express-as style="${AZURE_SPEAKING_STYLE}">`
            }
            ssmlText += stripUneeqTags(text)
            if (USE_SPEAKING_STYLE === "true") {
                ssmlText += '</mstts:express-as>'
            }
            if (PROSODY_SPEED.length > 0) {
                ssmlText += "</prosody>"
            }
            ssmlText += "</voice>"
            ssmlText += "</speak>"
        }

        synthesizer.speakSsmlAsync(

            ssmlText,
            result => {
                console.log(ssmlText);
                const {
                    audioData
                } = result;

                synthesizer.close();

                const bufferStream = new PassThrough();
                bufferStream.end(Buffer.from(audioData));
                resolve(bufferStream);

            },
            error => {
                console.log(error)
                synthesizer.close();
                reject(error);
            }
        );
    });
};

/*
 * This will strip all SSML (anything XML-style) and leave only the text string itself
 **/
const stripUneeqTags = (text) => {
    text = text.replace(/<uneeq:\w*>/gm, "");
    text = text.replace(/<\/uneeq:\w*>/gm, "");
    text = text.replace(/<[^>]*>/g, "");
    return text
}

module.exports = {
    textToSpeech,
    textToSpeechSsml,
    stripUneeqTags
};

デジタルヒューマンプラットフォームの設定

サービスがデプロイされ、到達可能であることが確認されたら、以下の詳細を弊社に提供してください:

  • サービスの完全なエンドポイントURL
  • サービスのAPIキー(任意)
  • ボイスファイル / 名前(Google、Amazon、Azureなどのプロバイダーに共通のもの。あなたのサービスにはこれが不要かもしれませんし、サービス内でこの値をハードコーディングすることもできます)

弊社は、あなたが作成したカスタム音声サービスをデジタルヒューマンで使用できるように構成します。次回デジタルヒューマンとのセッションを開始すると、カスタム音声が聞こえるようになります!

トラブルシューティング

このセクションでは、BYO TTSエンドポイントの構成または展開中に発生する一般的な問題とエラーをリストしています。ここにエラーが見当たらない場合、解決した後に追加することを検討してください。

 

デジタルヒューマンが非常に遅く/速く話している

これはサンプルレートの問題である可能性が高いです。16kHzのオーディオを返しているか確認してください。

 

スピーチの開始時にクリック音が聞こえる

これはレスポンスボディの冒頭に予期しないバイトがあるためです。以前、これがWAVファイルヘッダーであると確認されたことがあります。もし存在する場合は、返す前にこれを削除する必要があります。

 

聞こえるのはひどい雑音ばかり

オーディオが間違った形式で返されています。バイトオーダーがリトルエンディアンからビッグエンディアンに入れ替わるだけで起こることもありますし、mp3や他のオーディオ形式を返しているときなど、もっと複雑な場合もあります。私たちのアプリケーションはリニアPCMを期待していますので、「オーディオの返却」セクションを確認してください。

 

NodeJSとの互換性が悪い

NodeJSを使用している場合、Expressのres.send(file)をデフォルトで使用するかもしれませんが、バイナリファイルをストリーミングバックする際は、res.sendFile(file)を使用する方が良いです。これにより、さまざまな問題が自動的に処理されます:

  1. res.sendFile(file):
      • ファイルをレスポンスとして送信するために特化されています。ファイル拡張子に基づくContent-Typeや、Content-Dispositionを含む適切なヘッダーを自動的に設定し、ファイルのストリーミングを効率的に処理します。
      • 適切なヘッダーの設定、バッファリング、大きなファイルに対するチャンク単位での送信を担当し、メモリ効率を向上させます。
      • 条件付きリクエスト(If-Modified-Since、If-None-Match)とレンジ(Range、Accept-Ranges)を標準でサポートします。
      • パス正規化を使用して指定されたディレクトリ(ルートディレクトリ)外のファイルの提供を防ぎ、セキュリティを向上させます。
  1. res.send(file):
      • res.send(file)は、さまざまな種類のレスポンスを送信するために使用される汎用的なメソッドです。ただし、ファイルをres.sendFile(file)ほど最適化された方法で処理しません。
      • 適切なヘッダー(Content-TypeやContent-Dispositionなど)を手動で設定し、ファイルストリーミングのロジックも自分で処理する必要があります。
      • ファイル全体をメモリに読み込んでから送信する可能性があり、大きなファイルに対してはメモリを多く消費し、サーバーのパフォーマンスに影響を与える可能性があります。

要約すると、Expressでapplication/octet-streamとしてファイルをストリーミングする場合、res.sendFile(file)が推奨されます。これにより、パフォーマンス、メモリ効率、セキュリティが向上し、ファイル関連機能をより効果的に処理できます。

お役に立ちましたか?
😞
😐
🤩

最終更新日 June 14, 2024