Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.6.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<!--okhttp3-->
<!--dependency>
<groupId>com.squareup.okhttp3</groupId>
Expand Down
86 changes: 84 additions & 2 deletions src/main/java/top/rslly/iot/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,35 @@
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.std.BooleanSerializer;
import jakarta.annotation.Nullable;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching // 开启注解
public class RedisConfig extends CachingConfigurerSupport {

public class ByteArrayRedisSerializer implements RedisSerializer<byte[]> {

@Override
public byte[] serialize(@Nullable byte[] bytes) throws SerializationException {
// 如果输入是 null,返回 null 以匹配 Spring Data Redis 的约定
return bytes;
}

@Override
public byte[] deserialize(@Nullable byte[] bytes) throws SerializationException {
// 如果输入是 null,返回 null
return bytes;
}
}

/**
* retemplate相关配置
*
Expand Down Expand Up @@ -71,6 +87,72 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factor
return template;
}

@Bean
public RedisTemplate<String, byte[]> bytesRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, byte[]> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);

// Key 使用 String 序列化器
RedisSerializer<String> keySerializer = new StringRedisSerializer();
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);

// Value 使用 ByteArray 序列化器,直接存储字节数组
RedisSerializer<byte[]> valueSerializer = new ByteArrayRedisSerializer();
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);

template.afterPropertiesSet();
return template;
}

@Bean
public RedisTemplate<String, Boolean> boolRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Boolean> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);

// Key 使用 String 序列化器
RedisSerializer<String> keySerializer = new StringRedisSerializer();
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);

// Value 使用自定义 Boolean 序列化器
RedisSerializer<Boolean> valueSerializer = new BooleanRedisSerializer();
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);

template.afterPropertiesSet();
return template;
}

/**
* 自定义 Boolean 序列化器,将 Boolean 存储为 "1"/"0"
*/
public static class BooleanRedisSerializer implements RedisSerializer<Boolean> {

private static final byte[] TRUE_BYTES = "1".getBytes();
private static final byte[] FALSE_BYTES = "0".getBytes();

@Override
public byte[] serialize(@Nullable Boolean bool) throws RuntimeException {
if (bool == null) {
return new byte[0];
}
return bool ? TRUE_BYTES : FALSE_BYTES;
}

@Override
public Boolean deserialize(@Nullable byte[] bytes) throws RuntimeException {
if (bytes == null || bytes.length == 0) {
return null;
}
// 支持 "1" / "true" / "yes" / "on" 等,此处简单判断第一个字节是否为 '1'
return bytes.length > 0 && bytes[0] == '1';
}
}

/**
* 对hash类型的数据操作
*
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/top/rslly/iot/models/AgentMemoryEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void setChatId(String chatId) {
}

@Basic
@Column(name = "content")
@Column(name = "content", length = 1000)
public String getContent() {
return content;
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/top/rslly/iot/models/ProductRoleEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void setRole(String role) {
}

@Basic
@Column(name = "role_introduction")
@Column(name = "role_introduction", length = 6000)
public String getRoleIntroduction() {
return roleIntroduction;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void setId(int id) {
}

@Basic
@Column(name = "prompt")
@Column(name = "prompt", length = 2000)
public String getPrompt() {
return prompt;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class GoodByeTool implements BaseTool<String> {
"嘿嘿,我先走一步~",
"冲冲冲,下次见!",
"哼,本宝宝要走了!",
"嘻嘻,886!",
"嘻嘻,再见啦!",
"溜达溜达,回见!",
"嗖一下就消失啦~",
"哒哒哒,人家要跑了!",
Expand Down
41 changes: 27 additions & 14 deletions src/main/java/top/rslly/iot/utility/ai/voice/TTS/EdgeTTs.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import jakarta.websocket.Session;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
Expand All @@ -43,8 +44,28 @@ public class EdgeTTs implements TtsService {
@Override
public void websocketAudioSync(String text, Float pitch, Float speed, Session session,
String chatId, String voice) {
// Only used for WebSocket audio sending.
List<byte[]> audioList = getTextAudio(chatId, text, pitch, speed, voice);
final BlockingQueue<byte[]> audioQueue = new LinkedBlockingQueue<>();
for (byte[] b : audioList) {
audioQueue.offer(b);
}
try {
AudioUtils.asyncSendAudioQueue(chatId, session, audioQueue);
} catch (Exception e) {
log.error("websocketAudio error for chatId: {}", chatId, e);
}
}

@Override
public void asyncSynthesizeAndSaveAudio(String text, String chatId) {
throw new UnsupportedOperationException();
}

@Override
public List<byte[]> getTextAudio(String chatId, String text, Float pitch, Float speed,
String voice) {
// Only used for WebSocket audio sending.
List<byte[]> audioList = new ArrayList<>();
// End-of-stream marker: an empty byte array.
final byte[] EOS = new byte[0];
final OpusEncoderUtils encoder = new OpusEncoderUtils(16000, 1, 60);
Expand Down Expand Up @@ -86,17 +107,13 @@ public void websocketAudioSync(String text, Float pitch, Float speed, Session se
// 1. 将MP3转换为PCM (已经设置为16kHz采样率和单声道)
byte[] pcmData = AudioUtils.convertMp3ToPcm(fullPath);
List<byte[]> packets = encoder.encodePcmToOpus(pcmData, false);
for (byte[] packet : packets) {
audioQueue.offer(packet);
}
audioList.addAll(packets);
packets = encoder.encodePcmToOpus(new byte[0], true);
for (byte[] packet : packets) {
audioQueue.offer(packet);
}
audioList.addAll(packets);
// Signal end-of-stream by adding an empty array.
audioQueue.offer(EOS);
audioList.add(EOS);

AudioUtils.asyncSendAudioQueue(chatId, session, audioQueue);
return audioList;

} catch (Exception e) {
log.error("websocketAudio error for chatId: {}", chatId, e);
Expand All @@ -110,10 +127,6 @@ public void websocketAudioSync(String text, Float pitch, Float speed, Session se
}
}
}
}

@Override
public void asyncSynthesizeAndSaveAudio(String text, String chatId) {
throw new UnsupportedOperationException();
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

Expand Down Expand Up @@ -70,8 +72,35 @@ public class MiniMaxTtsService implements TtsService {
@Override
public void websocketAudioSync(String text, Float pitch, Float speed, Session session,
String chatId, String voice) {
List<byte[]> audioList = getTextAudio(chatId, text, pitch, speed, voice);
// Only used for WebSocket audio sending.
final BlockingQueue<byte[]> audioQueue = new LinkedBlockingQueue<>();
for (byte[] b : audioList) {
audioQueue.offer(b);
}
try {
// 异步发送音频队列
AudioUtils.asyncSendAudioQueue(chatId, session, audioQueue);
} catch (Exception e) {
log.error("MiniMax TTS error for chatId: {}", chatId, e);
}
}

/**
* 获取文本的Opus音频字节流
*
* @param chatId 对话ID
* @param text 文本
* @param pitch 语调
* @param speed 语速
* @param voice 声音类型
* @return Null或一个转载有Opus字节流的BlockingQueue
*/
@Override
public List<byte[]> getTextAudio(String chatId, String text, Float pitch, Float speed,
String voice) {
// Only used for WebSocket audio sending.
List<byte[]> audioList = new ArrayList<>();
// End-of-stream marker: an empty byte array.
final byte[] EOS = new byte[0];
final OpusEncoderUtils encoder = new OpusEncoderUtils(16000, 1, 60);
Expand Down Expand Up @@ -106,7 +135,7 @@ public void websocketAudioSync(String text, Float pitch, Float speed, Session se
errorReader.close();
log.error("MiniMax TTS API error for voice '{}': {} - {}", voice, responseCode,
errorResponse);
return;
return null;
}

log.debug("MiniMax TTS API request successful for voice: {}, text length: {}", voice,
Expand Down Expand Up @@ -163,37 +192,31 @@ public void websocketAudioSync(String text, Float pitch, Float speed, Session se
log.warn(
"MiniMax TTS returned empty audio data for voice: '{}', model: '{}', text length: {}",
voice, model != null && !model.isBlank() ? model : DEFAULT_MODEL, text.length());
return;
return null;
}
log.debug("MiniMax TTS generated {} bytes of audio data for voice: '{}'", mp3Data.length,
voice);

String outputPath = System.getProperty("java.io.tmpdir");
// 将 chatId 中的冒号替换为下划线,避免 Windows 路径非法字符问题
String safeChatId = chatId.replace(":", "_");
tempFilePath = Paths.get(outputPath, "minimax_tts_" + safeChatId + ".mp3").toString();
String uuid = UUID.randomUUID().toString();
tempFilePath = Paths.get(outputPath, uuid + ".mp3").toString();
Files.write(Paths.get(tempFilePath), mp3Data);

// 将 MP3 转换为 PCM (16kHz 单声道)
byte[] pcmData = AudioUtils.convertMp3ToPcm(tempFilePath);

// 编码为 Opus 并发送到队列
List<byte[]> packets = encoder.encodePcmToOpus(pcmData, false);
for (byte[] packet : packets) {
audioQueue.offer(packet);
}
audioList.addAll(packets);

// 刷新编码器
packets = encoder.encodePcmToOpus(new byte[0], true);
for (byte[] packet : packets) {
audioQueue.offer(packet);
}
audioList.addAll(packets);

// 信号结束
audioQueue.offer(EOS);
audioList.add(EOS);

// 异步发送音频队列
AudioUtils.asyncSendAudioQueue(chatId, session, audioQueue);
return audioList;

} catch (Exception e) {
log.error("MiniMax TTS error for chatId: {}", chatId, e);
Expand All @@ -208,6 +231,7 @@ public void websocketAudioSync(String text, Float pitch, Float speed, Session se
}
}
}
return null;
}

@Override
Expand Down
Loading
Loading