Skip to content

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 格式数据,参数说明如下:

参数名称类型说明默认值
textString要合成的文本内容。N/A
chunk_lengthInteger音频分片长度,取值范围 [100, 300](单位:毫秒或其他具体单位),建议 200。200
formatString输出音频格式,可选值:"wav", "pcm", "mp3"。"mp3"
mp3_bitrateInteger当选择 mp3 格式时指定比特率,可选值:64, 128, 192(单位:kbps)。128
referencesArray参考音频列表,用于上下文学习。数组中每个元素为对象,包含:[]
- audio (二进制数据):参考音频文件内容。
- text (String):参考音频对应说明文本。
reference_idString可选参数,若已存在参考音频资源,可传入其 ID(例如 7f92f8afb8ec43bf81429cc1c9199cb1)。null
normalizeBoolean是否对文本进行标准化处理(针对中英文及数字文本稳定性),推荐使用 true。true
latencyString延迟模式选择:"normal"(稳定模式)或 "balanced"(低延迟模式,约 300ms)。"normal"

1.4 响应说明 ​

接口响应为二进制流(音频数据),Java 程序员需要按照流的方式逐步读取数据并写入目标文件,例如将生成的 MP3 文件保存到本地。

1.5 Curl 示例 ​

下面的 curl 示例假设您已使用 msgpack 序列化工具(如 msgpack-java)生成了请求体文件 request.msgpack:

bash
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 请求。

java
package nexus.io.fishaudio.tts;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import nexus.io.model.http.response.ResponseVo;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.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 = nexus.io.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 参数来达到效果。

java
package nexus.io.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") 即可。

java
package nexus.io.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 的自动生成模板,从而避免模板编译错误。

java
package nexus.io.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 进行请求,以及如何使用不同的请求参数:

  1. 默认文本测试(直接传入文本内容):
  2. 指定参考语音 ID 测试(例如使用 Elon Musk 的声音,其 voice id 为 03397b4c4be74759b72533b663fbd001)。

同时测试用例在响应成功后将返回的二进制音频数据写入本地文件 output.mp3。

java
package nexus.io.manim.services;

import java.io.FileOutputStream;
import java.io.IOException;

import org.junit.Test;

import nexus.io.fishaudio.tts.FishAudioClient;
import nexus.io.fishaudio.tts.FishAudioTTSRequestVo;
import nexus.io.model.http.response.ResponseVo;
import nexus.io.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。

  • 错误处理:
    客户端封装中包含了错误码和响应体的处理逻辑,若请求失败可根据返回的错误信息进行排查和调整。

MIT Licensed