Java SDK

更新时间:
复制 MD 格式

使用 Java SDK 将文本合成为语音,按数据流接收音频,并按需配置音色、音频格式、语速和字幕。

前提条件

  • 已开通智能语音交互服务,并获取项目 Appkey。创建方法请参见创建项目。

  • 已准备具有调用权限的 AccessKey ID 和 AccessKey Secret,用于获取 NLS Token。Appkey、凭证和服务地址应使用同一套项目配置。

  • 已准备 Java 开发环境及 Maven 或 Gradle 项目。本文示例使用 JDK 21 和 nls-sdk-tts 2.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 写入代码或日志。

环境变量

说明

NLS_APP_KEY

智能语音交互项目的 Appkey。

ALIYUN_AK_ID

具有调用权限的 AccessKey ID。

ALIYUN_AK_SECRET

对应的 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、创建客户端及合成对象、设置参数、启动任务、处理回调并释放资源。

创建对象与鉴权

接口

说明

AccessToken(String accessKeyId, String accessKeySecret)

使用默认配置创建 Token 获取对象;构造方法不发送获取请求。服务配置请参见完整示例。

AccessToken(String accessKeyId, String accessKeySecret, String domain, String regionId, String version)

指定 Token 服务域名、地域和 API 版本。各参数应与所用服务配置一致。

AccessToken.apply()

发起 Token 获取请求。调用后检查 getToken() 是否为空,不应只以方法返回判断获取成功。

AccessToken.getToken()

返回已获取的 Token。

AccessToken.getExpireTime()

返回 Token 的过期时间戳。

NlsClient(String url, String token)

使用服务地址和 NLS Token 创建客户端。

NlsClient.setToken(String token)

更新客户端后续连接使用的 Token。

SpeechSynthesizer(NlsClient client, SpeechSynthesizerListener listener)

创建一个语音合成任务对象并建立连接。

合成参数

在调用 start() 前设置参数。下表为本文使用的主要参数。

参数

类型

说明

appKey(必选)

String

项目 Appkey,通过 setAppKey 设置。

text(必选)

String

通过 setText 设置待合成文本。文本采用 UTF-8 编码,长度不超过 300 个字符。英文单词间按正常书写添加空格。

voice(可选)

String

通过 setVoice 指定发音人。应选择支持合成文本语言的音色,示例显式设置为 siyue。

format(可选)

OutputFormatEnum

通过 setFormat 设置音频格式:PCM、WAV 或 MP3。默认 PCM。

sampleRate(可选)

SampleRateEnum

通过 setSampleRate 设置采样率:SAMPLE_RATE_16K 或 SAMPLE_RATE_8K。默认 16000 Hz。

volume(可选)

int

通过 setVolume 设置音量,范围 0~100,默认 50。

speechRate(可选)

int

通过 setSpeechRate 设置语速,范围 -500~500,默认 0。

pitchRate(可选)

int

通过 setPitchRate 设置语调,范围 -500~500,默认 0。

enable_subtitle(可选)

Boolean

通过 addCustomedParam 设置是否返回字级别时间戳,默认关闭。并非所有音色都支持字幕。

text 支持基础 SSML 标签,例如 <speak>Hello.<break time="1s"/>Welcome.</speak> 在两句话之间插入 1 秒停顿。

addCustomedParam(String key, Object value) 用于设置自定义请求参数,不限于字幕参数;应使用服务支持的参数名和值。字幕结果的说明,请参见语音合成时间戳功能介绍。

重要

不要根据成功状态码判断超长文本已完整合成。应在发起请求前控制文本长度,并核对实际音频内容。

使用多情感音色时,可在 text 中通过 SSML 的 emotion 标签指定情感,具体语法和音色要求请参见SSML 标记语言介绍。只有支持多情感的音色才能使用此标签,使用不支持的音色可能导致合成失败。

结果回调

实现 SpeechSynthesizerListener 的以下回调。onMetaInfo 在需要处理字幕时实现。

回调

说明

onMessage(ByteBuffer message)

接收音频二进制数据,可写入文件或交给播放器。

onComplete(SpeechSynthesizerResponse response)

接收合成结束事件,表示音频数据接收结束;同时检查状态码。

onFail(SpeechSynthesizerResponse response)

处理任务失败,记录 task_id、status 和 status_text 以便排查。

onMetaInfo(SpeechSynthesizerResponse response)

接收字幕信息,需要设置 enable_subtitle=true 且使用支持字幕的音色。

任务控制与资源释放

接口

说明

SpeechSynthesizer.start()

发送合成请求,不代表音频已经生成完毕。

SpeechSynthesizer.waitForComplete()

等待任务结束,没有等待超时上限。

SpeechSynthesizer.waitForComplete(long millsSeconds)

在指定时间内等待任务结束。SDK 2.1.7 起,时间单位由秒改为毫秒。方法返回后仍需检查成功或失败状态;超时返回不代表任务完成。

SpeechSynthesizer.close()

关闭当前任务连接。

NlsClient.shutdown()

应用退出时释放客户端资源。

常见问题

如何排查 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 发送记录和首个音频包接收记录。流式播放关注首包延迟,不应将完整文件生成时间作为首包延迟。