语音合成时间戳标记汉字或英文单词在合成音频中的起止时间,可用于视频字幕同步、文字高亮和虚拟人口型同步。通过请求参数开启该功能后,在接收音频流的同时获取时间戳。
使用限制
-
只有支持字级别音素边界接口的发音人才支持字级时间戳。
-
短文本语音合成 RESTful API 不返回时间戳信息。获取时间戳需使用 WebSocket 接口或相应 SDK。
-
字幕文本按发音生成,不一定与原文逐字对应。例如,
12可以对应“十”和“二”两个字幕单元。显示字幕时使用原始文本,并根据返回时间确定句首、句尾或高亮位置,不要直接将字幕索引作为原始字符串的字符偏移。
代码示例
以下 Java 示例使用 nls-sdk-tts 2.1.6,从环境变量读取项目 AppKey 和有效的 NLS Token,输出字幕信息,并将音频保存为 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,分别填写项目 AppKey 和 NLS Token。Token 必须有效且与项目所属账号匹配,不使用 STS Token 或其他产品的 API Key 代替。
合成并接收时间戳
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-cn-shanghai.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)) {
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");
}
System.out.println("Audio saved to tts_test.wav");
} finally {
synthesizer.close();
}
} finally {
client.shutdown();
}
}
}
合成成功后,程序输出 MetaInfo 字幕数组、SynthesisCompleted 及音频文件保存提示。首包延迟从发起合成前开始计时,到收到第一包音频结束;它不同于接收完整音频所需的时间。
示例将音频保存为文件。对实时性要求较高时,可在 onMessage 收到音频后边接收边播放,无需等到全部合成完成。
如果应用部署在阿里云上海地域的 ECS 上,并需要通过内网访问语音合成服务,可在创建 NlsClient 时使用以下服务地址:
NlsClient client = new NlsClient(
"ws://nls-gateway.cn-shanghai-internal.aliyuncs.com/ws/v1", token);
字级时间戳
参数设置
发起合成请求前,将 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 |
仅开启字级时间戳时,返回字符串 |
|
begin_index |
Integer |
文本单元的起始索引,从 0 开始,不是原始字符串的字符偏移。 |
|
end_index |
Integer |
文本单元的结束索引,不包含该位置。 |
例如,Hello world! 的两个单词分别对应索引区间 [0, 1) 和 [1, 2);你好,世界。 中“世”的起始索引为 2。
一次合成可能返回多个 MetaInfo 事件,其中的字幕可能重复或包含此前返回的内容。处理字幕时,根据索引和时间更新已有字幕数据,不要直接追加每次返回的整个数组。
返回示例
以下为合成 Hello world! 时返回的 payload 示例,时间值随音色、语速和文本变化。
{
"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
}
]
}
音素级时间戳
音素级时间戳提供文本单元内部各音素的起止时间,可用于更精细的口型同步。该能力取决于发音人:例如,aida、aiqi、aiyue、ailun、ava、emily 支持返回 phoneme_list;siyue、aicheng 在开启参数后返回音素字符串,但不返回音素时间戳列表。
移动端暂不支持音素级时间戳。
参数设置
发起合成请求前,同时将 enable_subtitle 和 enable_phoneme_timestamp 设置为 true。例如,使用 aida 合成中文:
synthesizer.setVoice("aida");
synthesizer.addCustomedParam("enable_subtitle", true);
synthesizer.addCustomedParam("enable_phoneme_timestamp", true);
服务端响应
音素信息仍通过 MetaInfo 事件的 payload.subtitles 返回,text、时间和索引字段与字级时间戳含义相同。音素相关字段如下:
|
字段 |
类型 |
说明 |
|
phoneme |
String |
文本单元对应的音素序列,以空格分隔。无音素信息时为字符串 |
|
phoneme_list |
List |
音素时间戳数组。是否返回取决于发音人,不能仅凭 |
phoneme_list 元素字段
|
字段 |
类型 |
说明 |
|
index |
Integer |
音素在当前文本单元内的序号,从 0 开始。 |
|
begin_time |
Integer |
音素在合成音频中的开始时间,单位为毫秒。 |
|
end_time |
Integer |
音素在合成音频中的结束时间,单位为毫秒。 |
|
phoneme |
String |
音素名称。映射关系请参见音素与拼音、国际音标的映射。 |
|
tone |
String |
音调或重音标记。英文中 |
返回示例
以下为 aida 合成“今天阳光很好。Hello world.”时返回的 payload 节选,仅保留“今”和“天”。
{
"subtitles": [
{
"text": "今",
"phoneme": "j_c in_c",
"begin_index": 0,
"end_index": 1,
"begin_time": 0,
"end_time": 200,
"phoneme_list": [
{
"index": 0,
"begin_time": 0,
"end_time": 104,
"phoneme": "j_c",
"tone": "1"
},
{
"index": 1,
"begin_time": 104,
"end_time": 200,
"phoneme": "in_c",
"tone": "1"
}
]
},
{
"text": "天",
"phoneme": "t_c ian_c",
"begin_index": 1,
"end_index": 2,
"begin_time": 200,
"end_time": 370,
"phoneme_list": [
{
"index": 0,
"begin_time": 200,
"end_time": 280,
"phoneme": "t_c",
"tone": "1"
},
{
"index": 1,
"begin_time": 280,
"end_time": 370,
"phoneme": "ian_c",
"tone": "1"
}
]
}
]
}
音素与拼音、国际音标的映射
中文音素使用映射表中的标记,例如“天”对应 t_c ian_c。部分拼音使用特殊标记,不能一律将拼音字母直接加上 _c。完整映射请参见拼音与音素映射表。
英文音素与国际音标的映射如下。部分音素名称附带数字,例如 ah1 表示 ah 的重音形式,ah0 表示轻音形式;数字 0、1、2 与 tone 的含义对应。
美式英文
|
序号 |
美式英文音素 |
国际音标(IPA) |
|
1 |
aa |
ɑ |
|
2 |
ae |
æ |
|
3 |
ah1 |
ʌ |
|
4 |
ah2 |
ə |
|
5 |
ah0 |
ə |
|
6 |
ao |
ɔ |
|
7 |
aw |
aʊ |
|
8 |
ay |
aɪ |
|
9 |
b |
b |
|
10 |
ch |
tʃ |
|
11 |
d |
d |
|
12 |
dh |
ð |
|
13 |
eh |
ɛ |
|
14 |
er |
ɝ |
|
15 |
ey |
eɪ |
|
16 |
f |
f |
|
17 |
g |
g |
|
18 |
hh |
h |
|
19 |
ih |
ɪ |
|
20 |
iy |
i |
|
21 |
jh |
dʒ |
|
22 |
k |
k |
|
23 |
l |
l |
|
24 |
m |
m |
|
25 |
n |
n |
|
26 |
ng |
ŋ |
|
27 |
ow |
oʊ |
|
28 |
oy |
ɔɪ |
|
29 |
p |
p |
|
30 |
r |
ɹ |
|
31 |
s |
s |
|
32 |
sh |
ʃ |
|
33 |
t |
t |
|
34 |
th |
θ |
|
35 |
uh |
ʊ |
|
36 |
uw |
u |
|
37 |
v |
v |
|
38 |
w |
w |
|
39 |
y |
j |
|
40 |
z |
z |
|
41 |
zh |
ʒ |
英式英文
|
序号 |
英式英文音素 |
国际音标(IPA) |
|
1 |
@ |
ə |
|
2 |
@@ |
ɜː |
|
3 |
a |
æ |
|
4 |
aa |
ɑː |
|
5 |
e |
ɛ |
|
6 |
i |
ɪ |
|
7 |
ii |
iː |
|
8 |
o |
ɒ |
|
9 |
oo |
ɔː |
|
10 |
u |
ʊ |
|
11 |
uh |
ʌ |
|
12 |
uu |
uː |
|
13 |
ai |
aɪ |
|
14 |
au |
aʊ |
|
15 |
e@ |
ɛə |
|
16 |
ei |
eɪ |
|
17 |
i@ |
ɪə |
|
18 |
oi |
ɔɪ |
|
19 |
ou |
əʊ |
|
20 |
u@ |
ʊə |
|
21 |
p |
p |
|
22 |
b |
b |
|
23 |
t |
t |
|
24 |
d |
d |
|
25 |
k |
k |
|
26 |
g |
g |
|
27 |
ch |
tʃ |
|
28 |
jh |
dʒ |
|
29 |
s |
s |
|
30 |
z |
z |
|
31 |
v |
v |
|
32 |
f |
f |
|
33 |
sh |
ʃ |
|
34 |
th |
θ |
|
35 |
dh |
ð |
|
36 |
zh |
ʒ |
|
37 |
h |
h |
|
38 |
r |
ɹ |
|
39 |
l |
l |
|
40 |
m |
m |
|
41 |
n |
n |
|
42 |
ng |
ŋ |
|
43 |
y |
j |
|
44 |
w |
w |