使用 Java SDK 将文本合成为语音,按数据流接收音频,并按需配置音色、音频格式、语速和字幕。
前提条件
已开通智能语音交互服务,并获取项目 Appkey。创建方法请参见创建项目。
已准备具有调用权限的 AccessKey ID 和 AccessKey Secret,用于获取 NLS Token。Appkey、凭证和服务地址应使用同一套项目配置。
已准备 Java 开发环境及 Maven 或 Gradle 项目。本文示例使用 JDK 21 和
nls-sdk-tts2.2.19。
安装 SDK
按安装 SDK、配置访问凭证、运行合成示例的顺序接入。请求参数、音色和服务地址的完整说明,请参见语音合成接口说明。
使用包管理器添加依赖,或下载示例项目。SDK 依赖 Netty;如果项目已引入 Netty,版本不得低于 4.1.17.Final。
Maven
在 pom.xml 的 dependencies 中添加以下依赖。jaxb-api 用于补齐示例在 JDK 21 下调用内置 AccessToken 所需的类。
<dependency>
<groupId>com.alibaba.nls</groupId>
<artifactId>nls-sdk-tts</artifactId>
<version>2.2.19</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
Gradle
在 build.gradle 中添加 Maven Central 仓库及以下依赖。jaxb-api 用于补齐示例在 JDK 21 下调用内置 AccessToken 所需的类。
repositories {
mavenCentral()
}
dependencies {
implementation 'com.alibaba.nls:nls-sdk-tts:2.2.19'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
}
示例项目
下载Java SDK 示例项目。该项目使用 SDK 2.2.1。
解压后,在 nls-sdk-java-demo 目录执行以下命令:
mvn package
语音合成可执行 JAR 位于 nls-example-tts/target/nls-example-tts-2.0.0-jar-with-dependencies.jar,该文件包含运行依赖。JAR 文件名中的 2.0.0 是示例项目版本,不是 SDK 版本。
在 JAR 所在目录调用合成主类时,需要传入 Appkey、NLS Token 和服务地址。先完成下文“配置访问凭证”中的示例项目配置,再执行以下命令:
java -cp nls-example-tts-2.0.0-jar-with-dependencies.jar \
com.alibaba.nls.client.SpeechSynthesizerDemo \
"$NLS_APP_KEY" "$NLS_TOKEN" "$NLS_GATEWAY_URL"
压测入口的参数依次为 Appkey、NLS Token、服务地址、合成文本、音频文件名和并发数:
java -jar nls-example-tts-2.0.0-jar-with-dependencies.jar \
"$NLS_APP_KEY" "$NLS_TOKEN" "$NLS_GATEWAY_URL" \
"Hello world." "tts-test.wav" 1
程序不提供交互式参数输入,缺少参数时会显示用法并退出。日志位于运行目录的 logs/nls.log。并发数应按已开通的服务规格设置。
示例项目通过命令行参数接收 Token,参数可能被本机进程查看工具读取。仅在受控环境中运行,不将含凭证的命令或日志公开。应用集成可使用本文从环境变量读取凭证的完整示例。
配置访问凭证
完整示例读取以下环境变量,并通过 SDK 获取 NLS Token。不要将 AccessKey 或 Token 写入代码或日志。
|
环境变量 |
说明 |
|
|
智能语音交互项目的 Appkey。 |
|
|
具有调用权限的 AccessKey ID。 |
|
|
对应的 AccessKey Secret。 |
有关 Token 获取方式和有效期,请参见获取 Token。
将占位值替换为实际配置,环境变量在当前终端会话中生效。使用 IDE 时,在运行配置中设置同名变量。
Linux / macOS
export NLS_APP_KEY="YOUR_APP_KEY"
export ALIYUN_AK_ID="YOUR_ACCESS_KEY_ID"
export ALIYUN_AK_SECRET="YOUR_ACCESS_KEY_SECRET"
Windows PowerShell
$env:NLS_APP_KEY="YOUR_APP_KEY"
$env:ALIYUN_AK_ID="YOUR_ACCESS_KEY_ID"
$env:ALIYUN_AK_SECRET="YOUR_ACCESS_KEY_SECRET"
Windows CMD
set NLS_APP_KEY=YOUR_APP_KEY
set ALIYUN_AK_ID=YOUR_ACCESS_KEY_ID
set ALIYUN_AK_SECRET=YOUR_ACCESS_KEY_SECRET
仅运行下载的示例项目时,还需配置 NLS_TOKEN 和 NLS_GATEWAY_URL:前者为已获取且未过期的 NLS Token,后者使用以下服务地址。完整 Java 示例已包含 Token 获取和服务地址配置,不读取这两个变量。
NLS_GATEWAY_URL:wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1。
Token 有有效期。长期运行的应用应在 Token 过期前重新获取,并通过 NlsClient.setToken 更新客户端后续连接使用的 Token;不要在日志中输出 Token 内容。
合成语音
以下示例获取 Token、初始化客户端,使用 siyue 合成 16 kHz WAV 音频,并保存到临时文件。程序记录首包延迟,并检查合成失败、等待超时和文件写入异常。
NlsClient 基于 Netty,创建成本较高且支持多线程共享。应用启动时创建并复用一个实例,退出时调用 shutdown()。每次合成使用独立的 SpeechSynthesizer 和监听器,任务结束后调用 close(),不要跨任务复用这两个对象。
import com.alibaba.nls.client.AccessToken;
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.Path;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class SpeechSynthesizerDemo {
private static String requireEnv(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 = requireEnv("NLS_APP_KEY");
String accessKeyId = requireEnv("ALIYUN_AK_ID");
String accessKeySecret = requireEnv("ALIYUN_AK_SECRET");
AccessToken accessToken = new AccessToken(accessKeyId, accessKeySecret);
accessToken.apply();
if (accessToken.getToken() == null || accessToken.getToken().isEmpty()) {
throw new IllegalStateException("Failed to obtain an NLS token");
}
NlsClient client = new NlsClient("wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1", accessToken.getToken());
try {
Path output = Files.createTempFile("tts-", ".wav");
try (OutputStream audio = Files.newOutputStream(output)) {
AtomicBoolean completed = new AtomicBoolean(false);
AtomicBoolean firstPacket = new AtomicBoolean(true);
AtomicReference<Exception> failure = new AtomicReference<>();
final long[] startedAt = new long[1];
SpeechSynthesizerListener listener = new SpeechSynthesizerListener() {
@Override
public void onMessage(ByteBuffer message) {
if (firstPacket.compareAndSet(true, false)) {
long elapsedMs = (System.nanoTime() - startedAt[0]) / 1_000_000;
System.out.println("First packet latency (ms): " + elapsedMs);
}
byte[] bytes = new byte[message.remaining()];
message.get(bytes);
try {
audio.write(bytes);
} catch (IOException e) {
failure.compareAndSet(null, e);
}
}
@Override
public void onComplete(SpeechSynthesizerResponse response) {
if (response.getStatus() == 20000000) {
completed.set(true);
System.out.println("Synthesis completed, task_id: " + response.getTaskId());
} else {
onFail(response);
}
}
@Override
public void onFail(SpeechSynthesizerResponse response) {
failure.compareAndSet(null, new IOException(
"task_id=" + response.getTaskId() + ", status=" + response.getStatus()
+ ", status_text=" + response.getStatusText()));
}
@Override
public void onMetaInfo(SpeechSynthesizerResponse response) {
System.out.println("Subtitles: " + response.getObject("subtitles"));
}
};
SpeechSynthesizer synthesizer = new SpeechSynthesizer(client, listener);
try {
synthesizer.setAppKey(appKey);
synthesizer.setVoice("siyue");
synthesizer.setFormat(OutputFormatEnum.WAV);
synthesizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_16K);
synthesizer.setVolume(50);
synthesizer.setSpeechRate(0);
synthesizer.setPitchRate(0);
synthesizer.setText("欢迎使用智能语音交互语音合成服务。");
synthesizer.addCustomedParam("enable_subtitle", false);
startedAt[0] = System.nanoTime();
synthesizer.start();
synthesizer.waitForComplete(30_000L);
if (failure.get() != null) {
throw failure.get();
}
if (!completed.get()) {
throw new TimeoutException("Synthesis did not complete within 30 seconds");
}
} finally {
synthesizer.close();
}
}
System.out.println("Audio file: " + output.toAbsolutePath());
} finally {
client.shutdown();
}
}
}
调用成功后,程序输出 Synthesis completed 和音频文件路径。打开生成的 WAV 文件检查音频内容。发生异常时,生成的临时文件可能不完整,不应作为合成结果使用。
示例等待上限为 30 秒,这是应用设置,不是服务耗时承诺。需要低延迟播放时,可在 onMessage 中边接收边播放;流式播放不改变输入文本的长度限制。
如果应用部署在上海地域的 ECS 上并需要内网访问,请从语音合成服务地址中选择适用的内网地址,不要将公网地址作为内网地址使用。
关键接口
调用流程为获取 Token、创建客户端及合成对象、设置参数、启动任务、处理回调并释放资源。
创建对象与鉴权
|
接口 |
说明 |
|
|
使用默认配置创建 Token 获取对象;构造方法不发送获取请求。服务配置请参见完整示例。 |
|
|
指定 Token 服务域名、地域和 API 版本。各参数应与所用服务配置一致。 |
|
|
发起 Token 获取请求。调用后检查 |
|
|
返回已获取的 Token。 |
|
|
返回 Token 的过期时间戳。 |
|
|
使用服务地址和 NLS Token 创建客户端。 |
|
|
更新客户端后续连接使用的 Token。 |
|
|
创建一个语音合成任务对象并建立连接。 |
合成参数
在调用 start() 前设置参数。下表为本文使用的主要参数。
|
参数 |
类型 |
说明 |
|
appKey(必选) |
String |
项目 Appkey,通过 |
|
text(必选) |
String |
通过 |
|
voice(可选) |
String |
通过 |
|
format(可选) |
OutputFormatEnum |
通过 |
|
sampleRate(可选) |
SampleRateEnum |
通过 |
|
volume(可选) |
int |
通过 |
|
speechRate(可选) |
int |
通过 |
|
pitchRate(可选) |
int |
通过 |
|
enable_subtitle(可选) |
Boolean |
通过 |
text 支持基础 SSML 标签,例如 <speak>Hello.<break time="1s"/>Welcome.</speak> 在两句话之间插入 1 秒停顿。
addCustomedParam(String key, Object value) 用于设置自定义请求参数,不限于字幕参数;应使用服务支持的参数名和值。字幕结果的说明,请参见语音合成时间戳功能介绍。
不要根据成功状态码判断超长文本已完整合成。应在发起请求前控制文本长度,并核对实际音频内容。
使用多情感音色时,可在 text 中通过 SSML 的 emotion 标签指定情感,具体语法和音色要求请参见SSML 标记语言介绍。只有支持多情感的音色才能使用此标签,使用不支持的音色可能导致合成失败。
结果回调
实现 SpeechSynthesizerListener 的以下回调。onMetaInfo 在需要处理字幕时实现。
|
回调 |
说明 |
|
|
接收音频二进制数据,可写入文件或交给播放器。 |
|
|
接收合成结束事件,表示音频数据接收结束;同时检查状态码。 |
|
|
处理任务失败,记录 |
|
|
接收字幕信息,需要设置 |
任务控制与资源释放
|
接口 |
说明 |
|
|
发送合成请求,不代表音频已经生成完毕。 |
|
|
等待任务结束,没有等待超时上限。 |
|
|
在指定时间内等待任务结束。SDK 2.1.7 起,时间单位由秒改为毫秒。方法返回后仍需检查成功或失败状态;超时返回不代表任务完成。 |
|
|
关闭当前任务连接。 |
|
|
应用退出时释放客户端资源。 |
常见问题
如何排查 ClosedChannelException?
检查网络连通性、服务地址、Token 有效期及依赖冲突,并与示例项目对照。task_id 由 SDK 在客户端发送请求前生成,仅凭其有无不能判断请求是否已成功到达服务端;应结合异常、服务端回调和日志定位。
如何排查缺少 JAXB 类的问题?
在 JDK 21 下调用内置 AccessToken 时,如果出现 NoClassDefFoundError: javax/xml/bind/DatatypeConverter,添加安装配置中的 jaxb-api 依赖,并确认该依赖包含在运行时类路径中。
如何排查 org.json.JSONArray.iterator()Ljava/util/Iterator 错误?
检查依赖包是否完整及是否存在版本冲突。对于使用以下 JSON 库的项目,核对实际加载的 JAR 版本。
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20170516</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
如何分析语音合成延迟?
首包延迟是从发送合成请求到收到第一个音频包的时间;完整合成延迟是从发送请求到收到合成结束事件的时间。应在 start() 前记录任务开始时间,在第一个 onMessage 和 onComplete 中分别计算,避免多个任务共享计时变量。
通过 SDK 日志分析时,定位同一 task_id 的 StartSynthesis 发送记录和首个音频包接收记录。流式播放关注首包延迟,不应将完整文件生成时间作为首包延迟。