Fish.audio TTS 接口说明文档与 Java 客户端封装
本文档详细说明了 Fish.audio TTS 接口的使用方法,并给出了完整的 Java 客户端封装示例代码,包括请求参数对象、硬编码实现的 MsgPack 转换工具、客户端调用逻辑以及测试示例。通过阅读本文档,您可以了解如何构造 msgpack 格式的请求体,如何使用 OkHttp 发起 HTTP 请求,以及如何指定参考声音(例如 Elon Musk 语音)。
本文的所有代码已经整合到了https://github.com/litongjava/java-openai
1. Fish.audio TTS 接口说明
接口地址: https://api.fish.audio/v1/tts
请求方法: POST
1.1 接口概述
该接口用于将文本转换为语音输出(TTS),支持流式传输返回音频数据,同时允许传入参考音频以辅助上下文学习。返回的响应是二进制音频流,适合用于大文件或实时语音传输。
1.2 请求头(HTTP Headers)
- authorization:
Bearer YOUR_API_KEY
替换YOUR_API_KEY
为实际的 API 密钥。 - content-type:
application/msgpack
数据传输采用 msgpack 序列化格式,Java 程序员可使用 msgpack-java 库生成请求体。 - model:
speech-1.6
指定使用的 TTS 模型版本。
1.3 请求体(Body)参数说明
请求体使用 msgpack 格式数据,参数说明如下:
参数名称 | 类型 | 说明 | 默认值 |
---|---|---|---|
text | String | 要合成的文本内容。 | N/A |
chunk_length | Integer | 音频分片长度,取值范围 [100, 300](单位:毫秒或其他具体单位),建议 200。 | 200 |
format | String | 输出音频格式,可选值:"wav" , "pcm" , "mp3" 。 | "mp3" |
mp3_bitrate | Integer | 当选择 mp3 格式时指定比特率,可选值:64, 128, 192(单位:kbps)。 | 128 |
references | Array | 参考音频列表,用于上下文学习。数组中每个元素为对象,包含: | [] |
- audio (二进制数据):参考音频文件内容。 | |||
- text (String):参考音频对应说明文本。 | |||
reference_id | String | 可选参数,若已存在参考音频资源,可传入其 ID(例如 7f92f8afb8ec43bf81429cc1c9199cb1 )。 | null |
normalize | Boolean | 是否对文本进行标准化处理(针对中英文及数字文本稳定性),推荐使用 true。 | true |
latency | String | 延迟模式选择:"normal" (稳定模式)或 "balanced" (低延迟模式,约 300ms)。 | "normal" |
1.4 响应说明
接口响应为二进制流(音频数据),Java 程序员需要按照流的方式逐步读取数据并写入目标文件,例如将生成的 MP3 文件保存到本地。
1.5 Curl 示例
下面的 curl 示例假设您已使用 msgpack 序列化工具(如 msgpack-java)生成了请求体文件 request.msgpack
:
curl -X POST "https://api.fish.audio/v1/tts" \
-H "authorization: Bearer YOUR_API_KEY" \
-H "content-type: application/msgpack" \
-H "model: speech-1.6" \
--data-binary @request.msgpack \
--output output.mp3
说明:
- 请将
YOUR_API_KEY
替换为实际的 API 密钥。 --data-binary @request.msgpack
表示请求体数据已保存在本地文件request.msgpack
中(必须为 msgpack 格式)。--output output.mp3
表示将接口返回的音频二进制流保存到output.mp3
文件中。
2. FishAudio Java 客户端封装
下面给出完整的客户端封装示例代码,包括业务逻辑、请求参数对象、MsgPack 硬编码转换工具以及测试用例。
2.1 FishAudioClient
该类封装了对 Fish.audio TTS 接口的调用,支持直接传入文本或构造完整的请求对象进行调用,内部通过 MsgPack 工具将请求对象序列化为 msgpack 二进制数据,并使用 OkHttp 发起 HTTP 请求。
package com.litongjava.fishaudio.tts;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.litongjava.model.http.response.ResponseVo;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.http.OkHttpClientPool;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* FishAudioClient 用于调用 Fish.audio 的 TTS 接口。
*/
public class FishAudioClient {
public static final String TTS_URL = "https://api.fish.audio/v1";
/**
* 直接传入文本内容构造请求(使用默认参数)。
*
* @param text 需要合成的文本
* @return ResponseVo 响应结果(成功时包含音频二进制数据)
*/
public static ResponseVo speech(String text) {
FishAudioTTSRequestVo req = new FishAudioTTSRequestVo().setText(text);
// 其他参数均采用默认值,如 chunk_length = 200, format = "mp3" 等
return speech(req);
}
/**
* 传入 FishAudioTTSRequestVo 对象。
*
* @param vo 请求对象
* @return ResponseVo 响应结果
*/
public static ResponseVo speech(FishAudioTTSRequestVo vo) {
String apiKey = EnvUtils.get("FISHAUDIO_API_KEY");
return speech(apiKey, vo);
}
/**
* 指定 API Key 调用接口。
*
* @param apiKey API密钥
* @param vo 请求对象
* @return ResponseVo 响应结果
*/
public static ResponseVo speech(String apiKey, FishAudioTTSRequestVo vo) {
String apiPrefixUrl = EnvUtils.get("FISHAUDIO_API_URL", TTS_URL);
return speech(apiPrefixUrl, apiKey, vo);
}
/**
* 完整的接口调用:指定 URL、API Key 和请求对象。
*
* @param apiPrefixUrl 接口前缀地址,如 https://api.fish.audio/v1
* @param apiKey API 密钥
* @param vo 请求对象
* @return ResponseVo 响应结果
*/
public static ResponseVo speech(String apiPrefixUrl, String apiKey, FishAudioTTSRequestVo vo) {
// 使用 msgpack 工具将请求对象序列化成二进制
byte[] payload = com.litongjava.fishaudio.tts.FishAudioMsgPackConverter.encodeFishAudioTTSRequestVo(vo);
return speechRequest(apiPrefixUrl, apiKey, payload);
}
/**
* 发起 HTTP 请求,返回鱼声平台 TTS 接口响应结果。
*
* @param apiPrefixUrl 接口前缀
* @param apiKey API 密钥
* @param payload msgpack 序列化后的请求数据
* @return ResponseVo 响应结果,成功时包含音频二进制数据
*/
public static ResponseVo speechRequest(String apiPrefixUrl, String apiKey, byte[] payload) {
// 接口地址为 “/tts”
String baseUrl = apiPrefixUrl + "/tts";
Map<String, String> header = new HashMap<>();
header.put("Authorization", "Bearer " + apiKey);
header.put("content-type", "application/msgpack");
// 指定 TTS 模型版本,默认 "speech-1.6"
header.put("model", "speech-1.6");
return execute(baseUrl, header, payload);
}
/**
* 发送 HTTP 请求并处理响应(流式返回音频数据)。
*
* @param url 完整 URL
* @param header 请求头信息
* @param payload 请求体(msgpack二进制数据)
* @return ResponseVo 响应结果
*/
private static ResponseVo execute(String url, Map<String, String> header, byte[] payload) {
MediaType mediaType = MediaType.parse("application/msgpack");
RequestBody body = RequestBody.create(payload, mediaType);
// 构建请求并添加请求头
Request.Builder requestBuilder = new Request.Builder().url(url).post(body);
for (Map.Entry<String, String> entry : header.entrySet()) {
requestBuilder.addHeader(entry.getKey(), entry.getValue());
}
Request request = requestBuilder.build();
OkHttpClient httpClient = OkHttpClientPool.get300HttpClient();
try (Response response = httpClient.newCall(request).execute()) {
int code = response.code();
if (response.isSuccessful()) {
// 成功时返回音频二进制数据
return ResponseVo.ok(response.body().bytes());
} else {
// 失败时返回错误码和响应体
String responseBody = response.body().string();
return ResponseVo.fail(code, responseBody);
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
2.2 FishAudioReferenceAudio
表示参考音频的实体,用于 in-context 学习。注意:当需要指定特定发音人(例如 Elon Musk),可通过设置 reference_id
参数来达到效果。
package com.litongjava.fishaudio.tts;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 表示参考音频,用于 in-context 学习。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class FishAudioReferenceAudio {
// 二进制格式的参考音频数据
private byte[] audio;
// 参考音频对应的文字描述
private String text;
}
2.3 FishAudioTTSRequestVo
此请求对象封装了调用 Fish.audio TTS 接口时需要提交的所有参数。通过设置 reference_id
,可以指定使用特定的发音人。例如,要使用 “Elon Musk(Noise reduction)” 的声音,其 voice id 为 03397b4c4be74759b72533b663fbd001
,只需调用 vo.setReference_id("03397b4c4be74759b72533b663fbd001")
即可。
package com.litongjava.fishaudio.tts;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 请求 Fish.audio TTS 接口时使用的参数对象。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class FishAudioTTSRequestVo {
// 合成的文本内容
private String text;
// 音频分片长度(范围 100 ~ 300),默认 200
private Integer chunk_length = 200;
// 输出音频格式,可选 "wav"、"pcm"、"mp3",默认 "mp3"
private String format = "mp3";
// 当 format 为 mp3 时使用的比特率,可选 64, 128, 192,默认 128
private Integer mp3_bitrate = 128;
// 参考音频列表,用于 in-context 学习,可传入多个
private List<FishAudioReferenceAudio> references;
// 直接指定参考音频的在线资源 id(例如 "7f92f8afb8ec43bf81429cc1c9199cb1"),可选
private String reference_id;
// 是否对文本进行标准化处理,默认为 true
private Boolean normalize = true;
// 延迟模式,"normal"(稳定模式)或 "balanced"(低延迟模式,约 300ms),默认 "normal"
private String latency = "normal";
}
2.4 FishAudioMsgPackConverter
该工具类采用硬编码方式将 FishAudioTTSRequestVo
对象转换为 msgpack 格式的字节数组,格式采用 Map 结构,与 ormsgpack.OPT_SERIALIZE_PYDANTIC 生成的数据格式保持一致。所有辅助方法均手工实现,不依赖 msgpack-java 的自动生成模板,从而避免模板编译错误。
package com.litongjava.fishaudio.tts;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* HardCodedMsgPackConverter 将 FishAudioTTSRequestVo 对象手动转换为 msgpack 格式字节数组,
* 采用 Map 结构(与 ormsgpack.OPT_SERIALIZE_PYDANTIC 生成的结构一致)。
*/
public class FishAudioMsgPackConverter {
/**
* 将 FishAudioTTSRequestVo 对象编码为 msgpack 格式的字节数组(Map 格式)。
*
* @param vo 请求对象
* @return msgpack 编码后的字节数组
*/
public static byte[] encodeFishAudioTTSRequestVo(FishAudioTTSRequestVo vo) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(baos);
// 我们将 vo 编码为一个 Map,共包含 8 个键值对
writeMapHeader(out, 8);
// 键 "text"
writeString(out, "text");
writeString(out, vo.getText());
// 键 "chunk_length"
writeString(out, "chunk_length");
writeInt(out, vo.getChunk_length());
// 键 "format"
writeString(out, "format");
writeString(out, vo.getFormat());
// 键 "mp3_bitrate"
writeString(out, "mp3_bitrate");
writeInt(out, vo.getMp3_bitrate());
// 键 "references"
writeString(out, "references");
List<FishAudioReferenceAudio> refs = vo.getReferences();
if (refs == null) {
writeArrayHeader(out, 0);
} else {
writeArrayHeader(out, refs.size());
for (FishAudioReferenceAudio ref : refs) {
encodeFishAudioReferenceAudio(out, ref);
}
}
// 键 "reference_id"
writeString(out, "reference_id");
if (vo.getReference_id() == null) {
writeNil(out);
} else {
writeString(out, vo.getReference_id());
}
// 键 "normalize"
writeString(out, "normalize");
writeBoolean(out, vo.getNormalize());
// 键 "latency"
writeString(out, "latency");
writeString(out, vo.getLatency());
out.flush();
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Error encoding FishAudioTTSRequestVo", e);
}
}
/**
* 将 FishAudioReferenceAudio 对象编码为 msgpack 格式数据(Map 格式),包含 2 个键值对:audio 和 text。
*/
private static void encodeFishAudioReferenceAudio(DataOutputStream out, FishAudioReferenceAudio ref) throws IOException {
// 编码为 Map,包含 2 个字段
writeMapHeader(out, 2);
// 键 "audio"
writeString(out, "audio");
byte[] audio = ref.getAudio();
if (audio == null) {
writeNil(out);
} else {
writeByteArray(out, audio);
}
// 键 "text"
writeString(out, "text");
writeString(out, ref.getText());
}
//////////////// 以下为 msgpack 编码辅助方法 ////////////////
// 写入 Map 头(如果 size < 16 使用 fixmap)
private static void writeMapHeader(DataOutputStream out, int size) throws IOException {
if (size < 16) {
out.writeByte(0x80 | size); // fixmap
} else if (size < 65536) {
out.writeByte(0xde);
out.writeShort(size);
} else {
out.writeByte(0xdf);
out.writeInt(size);
}
}
// 写入 Array 头(如果 size < 16 使用 fixarray)
private static void writeArrayHeader(DataOutputStream out, int size) throws IOException {
if (size < 16) {
out.writeByte(0x90 | size); // fixarray
} else if (size < 65536) {
out.writeByte(0xdc);
out.writeShort(size);
} else {
out.writeByte(0xdd);
out.writeInt(size);
}
}
// 写入 nil 标记
private static void writeNil(DataOutputStream out) throws IOException {
out.writeByte(0xc0);
}
// 写入 Boolean 值
private static void writeBoolean(DataOutputStream out, boolean value) throws IOException {
out.writeByte(value ? 0xc3 : 0xc2);
}
// 写入整型数值(这里只处理正数,适用于本例中的 chunk_length 和 mp3_bitrate)
private static void writeInt(DataOutputStream out, int value) throws IOException {
if (value >= 0 && value < 128) {
out.writeByte(value); // positive fixnum
} else if (value < 256) {
out.writeByte(0xcc);
out.writeByte(value);
} else if (value < 65536) {
out.writeByte(0xcd);
out.writeShort(value);
} else {
out.writeByte(0xce);
out.writeInt(value);
}
}
// 写入字符串
private static void writeString(DataOutputStream out, String str) throws IOException {
if (str == null) {
writeNil(out);
return;
}
byte[] utf8 = str.getBytes(StandardCharsets.UTF_8);
int length = utf8.length;
if (length < 32) {
out.writeByte(0xa0 | length); // fixstr
} else if (length < 256) {
out.writeByte(0xd9);
out.writeByte(length);
} else if (length < 65536) {
out.writeByte(0xda);
out.writeShort(length);
} else {
out.writeByte(0xdb);
out.writeInt(length);
}
out.write(utf8);
}
// 写入 byte 数组(二进制数据)
private static void writeByteArray(DataOutputStream out, byte[] data) throws IOException {
int length = data.length;
if (length < 256) {
out.writeByte(0xc4);
out.writeByte(length);
} else if (length < 65536) {
out.writeByte(0xc5);
out.writeShort(length);
} else {
out.writeByte(0xc6);
out.writeInt(length);
}
out.write(data);
}
}
3. 测试用例
以下测试类演示了如何调用 FishAudioClient 进行请求,以及如何使用不同的请求参数:
- 默认文本测试(直接传入文本内容):
- 指定参考语音 ID 测试(例如使用 Elon Musk 的声音,其 voice id 为
03397b4c4be74759b72533b663fbd001
)。
同时测试用例在响应成功后将返回的二进制音频数据写入本地文件 output.mp3
。
package com.litongjava.manim.services;
import java.io.FileOutputStream;
import java.io.IOException;
import org.junit.Test;
import com.litongjava.fishaudio.tts.FishAudioClient;
import com.litongjava.fishaudio.tts.FishAudioTTSRequestVo;
import com.litongjava.model.http.response.ResponseVo;
import com.litongjava.tio.utils.environment.EnvUtils;
public class FishAudioClientTest {
@Test
public void fishAudioTest() {
EnvUtils.load();
ResponseVo responseVo = FishAudioClient.speech("今天天气怎么样");
if (responseVo.isOk()) {
byte[] audioBytes = responseVo.getBodyBytes();
try (FileOutputStream fos = new FileOutputStream("output.mp3")) {
fos.write(audioBytes);
System.out.println("MP3 文件已保存到 output.mp3");
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.err.println("请求失败:" + responseVo);
}
}
@Test
public void testWithReferenceId() {
EnvUtils.load();
// 构造请求对象,并指定参考语音ID(发音人)
FishAudioTTSRequestVo vo = new FishAudioTTSRequestVo();
vo.setText("今天天气怎么样");
vo.setReference_id("03397b4c4be74759b72533b663fbd001");
// 其它参数保持默认或根据需要进行设置,例如:
vo.setChunk_length(200);
vo.setFormat("mp3");
vo.setMp3_bitrate(128);
// 如果有需要使用参考音频(in-context learning),也可以通过 vo.setReferences(...) 传入对应参考语音数据
// 使用 FishAudioClient 发起请求
ResponseVo responseVo = FishAudioClient.speech(vo);
if (responseVo.isOk()) {
// 处理返回的音频数据,例如保存到文件
byte[] audioBytes = responseVo.getBodyBytes();
try (FileOutputStream fos = new FileOutputStream("output.mp3")) {
fos.write(audioBytes);
System.out.println("MP3 文件已保存到 output.mp3");
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.err.println("请求失败:" + responseVo.getBodyString());
}
}
}
3.1 环境变量配置
在项目中请确保配置了环境变量(例如使用 .env 文件):
FISHAUDIO_API_KEY=your_api_key_here
EnvUtils 工具会加载这些变量供客户端使用。
4. 说明与注意事项
MsgPack 序列化:
为保证服务器端能够正确解析请求体,本例中采用硬编码方式将 Java 实体转换为 msgpack 格式数据,其格式为 Map 结构,与 Fish.audio 平台预期的格式一致。指定不同发音人:
若需要使用指定发音人(如 “Elon Musk(Noise reduction)”),只需在请求对象中设置reference_id
为对应的声音 ID(例如"03397b4c4be74759b72533b663fbd001"
),服务器便会使用指定声音生成语音。请求头配置:
请确保在 HTTP 请求时设置正确的请求头,特别是authorization
,content-type
和model
。错误处理:
客户端封装中包含了错误码和响应体的处理逻辑,若请求失败可根据返回的错误信息进行排查和调整。