整合 fishaudio 到 java-uni-ai-server 项目
本文档详细介绍了如何在项目中整合 fishaudio 语音合成服务。项目原有的 TTS(语音合成)服务支持 openai 和 volce 平台,现在新增 fishaudio 服务,方便用户选择不同的 TTS 平台。下面将逐步展示各个模块的代码及功能说明。
1. 项目模块介绍
本次整合涉及到以下主要模块:
TTSPlatform 接口
定义了所有支持的 TTS 平台常量,包括新增的 fishaudio。ManimTTSHandler 请求处理器
负责接收客户端请求,从 HTTP 请求中提取参数,调用服务层生成语音,并返回生成的 MP3 文件。ManimTTSService 服务实现
核心业务逻辑模块。该模块首先检查是否存在缓存(通过 MD5 校验输入文本);若缓存存在则直接读取缓存文件。若缓存不存在,则根据请求选择调用不同的 TTS API(包括 fishaudio、volce 和 openai),生成语音文件后存储到本地并写入数据库缓存记录。
2. 代码详解与说明
2.1 TTSPlatform 接口
该接口定义了所有支持的 TTS 服务平台常量。在新增 fishaudio 整合时,我们只需在接口中增加一个对应的常量。
package com.litongjava.uni.consts;
public interface TTSPlatform {
String volce = "volce";
String openai = "openai";
String fishaudio = "fishaudio";
}
说明
- 此接口中定义了三个字符串常量:
volce
、openai
和fishaudio
。 - 在后续代码中,通过比较传入的
provider
参数与这些常量来决定调用哪个平台的 TTS 服务。
2.2 ManimTTSHandler 请求处理器
该类是 HTTP 请求的入口,负责从请求中获取参数,然后调用 ManimTTSService
的 tts
方法生成语音,再将返回的字节数组组装成 MP3 音频响应返回给客户端。
package com.litongjava.uni.handler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.server.util.Resps;
import com.litongjava.uni.services.ManimTTSService;
public class ManimTTSHandler {
ManimTTSService manimTTSService = Aop.get(ManimTTSService.class);
public HttpResponse index(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
String input = request.getParam("input");
String platform = request.getParam("platform");
String voice_id = request.getParam("voice_id");
byte[] audio = manimTTSService.tts(input, platform, voice_id);
return Resps.bytesWithContentType(response, audio, "audio/mp3");
}
}
说明
参数说明:
input
:待合成的文本,例如“今天天气怎么样”platform
:语音合成平台(如fishaudio
、openai
或volce
)voice_id
:指定使用的发音人 ID
执行流程:
- 从 HTTP 请求中读取参数。
- 调用
ManimTTSService.tts
方法处理逻辑,生成音频数据。 - 使用工具方法
Resps.bytesWithContentType
返回设置audio/mp3
Content-Type 的响应。
2.3 ManimTTSService 服务实现
这是核心业务逻辑代码。服务主要功能包括:
- 缓存判断:通过输入文本的 MD5 值检查是否已有生成的音频文件缓存(存储在数据库中)。如果缓存存在且文件有效,则直接返回该文件内容,节省调用 TTS 接口时间。
- 音频生成:若缓存不存在,则根据选择的 TTS 平台调用相应的接口。
- 如果选择
volce
平台,则调用VolceTtsClient.tts
。 - 如果选择
fishaudio
平台,则构造FishAudioTTSRequestVo
对象,并调用FishAudioClient.speech
生成语音。 - 其他情况,默认使用
openai
平台调用OpenAiTTSClient.speech
。
- 如果选择
- 缓存写入:生成音频后,将文件保存在本地,并将缓存信息写入数据库中,便于后续复用。
package com.litongjava.uni.services;
import java.io.File;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.fishaudio.tts.FishAudioClient;
import com.litongjava.fishaudio.tts.FishAudioTTSRequestVo;
import com.litongjava.model.http.response.ResponseVo;
import com.litongjava.openai.tts.OpenAiTTSClient;
import com.litongjava.tio.utils.crypto.Md5Utils;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import com.litongjava.uni.consts.TTSPlatform;
import com.litongjava.uni.consts.UniTableName;
import com.litongjava.volcengine.VolceTtsClient;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ManimTTSService {
public byte[] tts(String input, String provider, String voice_id) {
if (StrUtil.isBlank(provider)) {
provider = "openai";
}
if (StrUtil.isBlank(voice_id)) {
voice_id = "shimmer";
}
log.info("input:{},{},{}", input, provider, voice_id);
String md5 = Md5Utils.getMD5(input);
String sql = " select id,path from %s where md5=? and provider=? and voice=?";
sql = String.format(sql, UniTableName.UNI_TTS_CACHE);
Row row = Db.findFirst(sql, md5, provider, voice_id);
if (row != null) {
String path = row.getStr("path");
Long id = row.getLong("id");
if (path != null) {
File file = new File(path);
if (file.exists()) {
log.info("read file:{}", path);
return FileUtil.readBytes(file);
} else {
sql = " delete from %s where id=?";
sql = String.format(sql, UniTableName.UNI_TTS_CACHE);
Db.delete(sql, id);
}
}
}
byte[] bodyBytes = null;
long id = SnowflakeIdUtils.id();
String path = "cache" + File.separator + id + ".mp3";
if (TTSPlatform.volce.equals(provider)) {
bodyBytes = VolceTtsClient.tts(input);
} else if (TTSPlatform.fishaudio.equals(provider)) {
// 构造请求对象,并指定参考语音ID(发音人)
FishAudioTTSRequestVo vo = new FishAudioTTSRequestVo();
vo.setText(input);
vo.setReference_id(voice_id);
// 使用 FishAudioClient 发起请求
ResponseVo responseVo = FishAudioClient.speech(vo);
if (responseVo.isOk()) {
// 处理返回的音频数据,例如保存到文件
bodyBytes = responseVo.getBodyBytes();
} else {
log.error(responseVo.getBodyString());
path = "default.mp3";
return FileUtil.readBytes(new File(path));
}
} else {
ResponseVo responseVo = OpenAiTTSClient.speech(input);
if (responseVo.isOk()) {
bodyBytes = responseVo.getBodyBytes();
} else {
log.error(responseVo.getBodyString());
path = "default.mp3";
return FileUtil.readBytes(new File(path));
}
}
File file = new File(path);
FileUtil.writeBytes(bodyBytes, file);
Row saveRow = Row.by("id", id).set("input", input).set("md5", md5).set("path", path)
//
.set("provider", provider).set("voice", voice_id);
Db.save(UniTableName.UNI_TTS_CACHE, saveRow);
return bodyBytes;
}
}
说明
缓存流程:
- 通过
Md5Utils.getMD5(input)
生成输入文本的 MD5 值。 - 在数据库表
UNI_TTS_CACHE
中查找是否存在对应记录,若存在则直接读取缓存文件。 - 如果缓存文件不存在则删除该记录,防止出现脏数据。
- 通过
TTS 接口调用:
- 判断
provider
参数,如果为空默认为openai
;如果voice_id
为空则使用默认值 "shimmer"。 - 当
provider
为volce
时,调用VolceTtsClient.tts(input)
方法。 - 当
provider
为fishaudio
时,构造FishAudioTTSRequestVo
对象,设置text
和reference_id
,再调用FishAudioClient.speech(vo)
发起请求;若调用失败则返回默认音频文件。 - 其他情况默认调用 openai 服务。
- 判断
文件保存与缓存写入:
- 使用雪花算法生成唯一 ID,以此构造唯一文件名存储音频文件(保存于
cache
目录下)。 - 将音频写入文件后,记录缓存信息(包括输入、MD5 值、文件路径、provider 和 voice_id)写入数据库,便于下次直接复用。
- 使用雪花算法生成唯一 ID,以此构造唯一文件名存储音频文件(保存于
3. 请求测试说明
完成上述代码整合后,可通过以下接口进行测试:
- 请求 URL:
/api/manim/tts
- 请求方法:支持 GET 或 POST
- 请求参数:
- token:
123456
- input:
今天天气怎么样
- platform:
fishaudio
- voice_id:
03397b4c4be74759b72533b663fbd001
- token:
测试时,服务将调用 fishaudio 平台的 TTS 接口生成语音,返回音频数据格式为 audio/mp3
。如果过程中出现错误(例如 fishaudio 语音合成失败),则返回默认的 default.mp3
文件。
4. 总结
本文档详细说明了如何将 fishaudio 整合到 java-uni-ai-server 中,从常量定义、请求处理、服务层逻辑到缓存机制,所有代码均未省略且做了必要的注释和说明。开发者可以参考此文档了解整合逻辑,并根据需求对代码进行扩展和调整。
该整合方案通过多平台支持和缓存机制,不仅增加了系统的灵活性,还提高了整体性能和用户体验,希望对后续项目开发和维护有所帮助。