音声合成タイムスタンプは、合成音声内の各文字または英単語の開始時刻と終了時刻を示します。このタイムスタンプを使うと、ビデオ字幕、テキストのハイライト、バーチャルキャラクターの口の動きを同期できます。合成リクエストでタイムスタンプを有効にすると、オーディオストリームとともにタイミングデータを受け取れます。
制限事項
-
単語レベルのタイムスタンプは、単語レベルの音素境界をサポートする音声でのみ利用できます。
-
短文音声合成 RESTful API はタイムスタンプを返しません。タイムスタンプを取得するには、WebSocket API または対応する SDK を使用してください。
-
字幕テキストは発音に従うため、元のテキストと一文字ずつ一致しない場合があります。例えば、中国語の音声では、
12は十と二という 2 つの字幕単位で表現されることがあります。元のテキストを表示し、返されたタイミングデータを使用して文の境界やハイライト時間を決定してください。字幕インデックスを元の文字列における文字オフセットとして扱わないでください。
コード例
この Java の例では、nls-sdk-tts 2.1.6 を使用します。プロジェクトのアプリケーションキーと有効な NLS トークンを環境変数から読み取り、字幕情報を出力し、音声を tts_test.wav に保存します。同名の既存ファイルは上書きされます。
依存関係の追加と認証情報の設定
Maven プロジェクトの dependencies セクションに、以下の依存関係を追加します。
<dependency>
<groupId>com.alibaba.nls</groupId>
<artifactId>nls-sdk-tts</artifactId>
<version>2.1.6</version>
</dependency>
ランタイム環境で NLS_APP_KEY と NLS_TOKEN を、プロジェクトのアプリケーションキーと NLS トークンに設定します。トークンは有効であり、プロジェクトと同じアカウントに属している必要があります。STS トークンや他の製品の API キーを代用しないでください。
音声合成とタイムスタンプの取得
import com.alibaba.fastjson.JSONArray;
import com.alibaba.nls.client.protocol.NlsClient;
import com.alibaba.nls.client.protocol.OutputFormatEnum;
import com.alibaba.nls.client.protocol.SampleRateEnum;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizer;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizerListener;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizerResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class SpeechSynthesizerDemo {
private static String requiredEnv(String name) {
String value = System.getenv(name);
if (value == null || value.trim().isEmpty()) {
// 環境変数がありません:
throw new IllegalArgumentException("Missing environment variable: " + name);
}
return value;
}
public static void main(String[] args) throws Exception {
String appKey = requiredEnv("NLS_APP_KEY");
String token = requiredEnv("NLS_TOKEN");
String endpoint = "wss://nls-gateway-ap-southeast-1.aliyuncs.com/ws/v1";
NlsClient client = new NlsClient(endpoint, token);
try (OutputStream audio = Files.newOutputStream(Paths.get("tts_test.wav"))) {
AtomicReference<String> failure = new AtomicReference<>();
AtomicBoolean completed = new AtomicBoolean(false);
AtomicBoolean firstAudio = new AtomicBoolean(true);
final long start = System.nanoTime();
SpeechSynthesizerListener listener = new SpeechSynthesizerListener() {
@Override
public void onMessage(ByteBuffer message) {
if (firstAudio.compareAndSet(true, false)) {
// 最初の音声のレイテンシー (ms):
System.out.println("First audio latency (ms): "
+ (System.nanoTime() - start) / 1_000_000);
}
byte[] bytes = new byte[message.remaining()];
message.get(bytes);
try {
audio.write(bytes);
} catch (IOException e) {
// 音声の書き込みに失敗しました:
failure.compareAndSet(null, "Failed to write audio: " + e.getMessage());
}
}
@Override
public void onMetaInfo(SpeechSynthesizerResponse response) {
JSONArray subtitles = (JSONArray) response.getObject("subtitles");
if (subtitles != null) {
System.out.println("MetaInfo: " + subtitles.toJSONString());
}
}
@Override
public void onComplete(SpeechSynthesizerResponse response) {
completed.set(true);
System.out.println("SynthesisCompleted: " + response.getTaskId());
}
@Override
public void onFail(SpeechSynthesizerResponse response) {
failure.set("task_id=" + response.getTaskId()
+ ", status=" + response.getStatus()
+ ", status_text=" + response.getStatusText());
}
};
SpeechSynthesizer synthesizer = new SpeechSynthesizer(client, listener);
try {
synthesizer.setAppKey(appKey);
synthesizer.setFormat(OutputFormatEnum.WAV);
synthesizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_16K);
synthesizer.setVoice("siyue");
synthesizer.setPitchRate(100);
synthesizer.setSpeechRate(100);
synthesizer.setText("Hello world. I have 12 apples.");
synthesizer.addCustomedParam("enable_subtitle", true);
synthesizer.start();
synthesizer.waitForComplete();
if (failure.get() != null) {
throw new IllegalStateException(failure.get());
}
if (!completed.get()) {
// 合成が完了しませんでした
throw new IllegalStateException("Synthesis did not complete");
}
// 音声は tts_test.wav に保存されました
System.out.println("Audio saved to tts_test.wav");
} finally {
synthesizer.close();
}
} finally {
client.shutdown();
}
}
}
成功すると、プログラムは MetaInfo の字幕配列、SynthesisCompleted、および音声ファイルの確認メッセージを出力します。最初の音声のレイテンシーは、音声合成の開始前から最初の音声チャンクが到着するまでの時間として測定されます。これは、すべての音声を受信するまでに必要な時間とは異なります。
この例では音声をファイルに保存しています。レイテンシーが重要な再生処理の場合は、合成が完了するまで待つのではなく、onMessage で到着した音声チャンクを順次再生してください。
単語レベルのタイムスタンプ
リクエストパラメータ
合成を開始する前に、enable_subtitle を true に設定します。デフォルトでは、タイムスタンプは無効になっています。Java SDK では、以下を使用します。
synthesizer.addCustomedParam("enable_subtitle", true);
サーバーレスポンス
サービスは、MetaInfo イベントでタイムスタンプを返します。各イベントには、payload.subtitles に字幕アレイが含まれています。 Java SDK は、onMetaInfo コールバックを介してこれらのイベントを配信します。
各 subtitles 要素のフィールド
|
フィールド |
タイプ |
説明 |
|
text |
String |
発音ベースのテキスト単位 (漢字や英単語など) |
|
begin_time |
Integer |
合成音声におけるテキスト単位の開始時間 (ミリ秒単位) |
|
end_time |
Integer |
合成音声におけるテキスト単位の終了時間 (ミリ秒単位) |
|
phoneme |
String |
単語レベルのタイムスタンプのみが有効になっている場合、このフィールドには JSON の |
|
begin_index |
Integer |
テキスト単位のゼロベースの開始インデックス。これは、元の文字列への文字オフセットではありません。 |
|
end_index |
Integer |
テキスト単位の排他的な終了インデックス |
例えば、Hello world! 内の 2 つの単語は、インデックス範囲 [0, 1) および [1, 2) を占めます。句読点もインデックス位置を占めることがあります。中国語の入力 你好,世界。 では、文字 世 の開始インデックスは 3 です。
合成リクエストは、複数の MetaInfo イベントを生成できます。字幕配列は、重複する、または以前に返されたエントリを含むことがあります。各配列全体を追加するのではなく、インデックスとタイムスタンプを使用して既存のデータを更新してください。
レスポンス例
以下のサンプル payload は、Hello world! に対して返されます。タイミング値は、音声、話速、およびテキストによって異なります。
{
"subtitles": [
{
"text": "Hello",
"phoneme": "null",
"begin_index": 0,
"end_index": 1,
"begin_time": 0,
"end_time": 328
},
{
"text": "world",
"phoneme": "null",
"begin_index": 1,
"end_index": 2,
"begin_time": 328,
"end_time": 775
}
]
}