Skip to content

Commit db0df3c

Browse files
author
wangshuguang
committed
Merge remote-tracking branch 'source/main'
2 parents 7475eb0 + 13ed9a4 commit db0df3c

File tree

8 files changed

+105
-71
lines changed

8 files changed

+105
-71
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
# 变更日志
2+
## [2.7.64] - 2025-06-12
3+
4+
### 修复
5+
- Merge pull request #98 from vritser/main
6+
- fix(audio): merge audio files
7+
8+
### 其他变更
9+
- chore: update version to 2.7.64 [skip ci]
10+
- docs: update changelog for v2.7.63 [skip ci]
11+
- chore: update version to 2.7.63 [skip ci]
12+
13+
## [2.7.63] - 2025-06-12
14+
15+
### 其他变更
16+
- chore: update version to 2.7.63 [skip ci]
17+
218
## [2.7.62] - 2025-06-11
319

420
### 其他变更

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</parent>
1010
<groupId>com.xiaozhi.server</groupId>
1111
<artifactId>xiaozhi.server</artifactId>
12-
<version>2.7.62</version>
12+
<version>2.7.64</version>
1313
<name>xiaozhi-server</name>
1414
<description></description>
1515

src/main/java/com/xiaozhi/controller/ConfigController.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ public AjaxResult update(SysConfig config) {
7474
int rows = configService.update(config);
7575
if (rows > 0) {
7676
configManager.getConfig(config.getConfigId());// 更新缓存
77-
if(oldSysConfig != null){
78-
if("stt".equals(oldSysConfig.getConfigType()) && !oldSysConfig.getApiKey().equals(config.getApiKey())){
77+
if (oldSysConfig != null) {
78+
if ("stt".equals(oldSysConfig.getConfigType())
79+
&& !oldSysConfig.getApiKey().equals(config.getApiKey())) {
7980
sttServiceFactory.removeCache(oldSysConfig);
80-
}else if("tts".equals(oldSysConfig.getConfigType()) && !oldSysConfig.getApiKey().equals(config.getApiKey())){
81+
} else if ("tts".equals(oldSysConfig.getConfigType())
82+
&& !oldSysConfig.getApiKey().equals(config.getApiKey())) {
8183
ttsServiceFactory.removeCache(oldSysConfig);
8284
}
8385
}

src/main/java/com/xiaozhi/dialogue/service/DialogueService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ private void saveAssistantResponse(ChatSession session) {
731731

732732
// 合并音频文件
733733
if (!audioFilesToMerge.isEmpty()) {
734-
String mergedAudioPath = AudioUtils.AUDIO_PATH + AudioUtils.mergeWavFiles(audioFilesToMerge);
734+
String mergedAudioPath = AudioUtils.AUDIO_PATH + AudioUtils.mergeAudioFiles(audioFilesToMerge);
735735

736736
// 保存合并后的音频路径
737737
session.setAssistantAudioPath(mergedAudioPath);

src/main/java/com/xiaozhi/dialogue/tts/factory/TtsServiceFactory.java

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ public class TtsServiceFactory {
1818

1919
private static final Logger logger = LoggerFactory.getLogger(TtsServiceFactory.class);
2020

21-
// 缓存已初始化的服务:对于API服务,键为"provider:configId"格式;对于本地服务,键为provider名称
21+
// 缓存已初始化的服务:键为"provider:configId:voiceName"格式,确保音色变化时创建新实例
2222
private final Map<String, TtsService> serviceCache = new ConcurrentHashMap<>();
2323

2424
// 语音生成文件保存地址
25-
private static final String OUT_PUT_PATH = "audio/";
25+
private static final String OUTPUT_PATH = "audio/";
2626

2727
// 默认服务提供商名称
2828
private static final String DEFAULT_PROVIDER = "edge";
@@ -38,27 +38,32 @@ public TtsService getDefaultTtsService() {
3838
return getTtsService(config, TtsServiceFactory.DEFAULT_VOICE);
3939
}
4040

41-
// 对于API服务,使用"provider:configId"作为缓存键,确保每个配置使用独立的服务实例
42-
private String createCacheKey(SysConfig config, String provider) {
43-
var configId = config.getConfigId();
44-
var configIdStr = configId != null ? String.valueOf(configId) : "default";
45-
return provider + ":" + configIdStr;
41+
// 创建缓存键,包含provider、configId和voiceName,确保音色变化时创建新的服务实例
42+
private String createCacheKey(SysConfig config, String provider, String voiceName) {
43+
Integer configId = -1;
44+
if (config != null && config.getConfigId() != null) {
45+
configId = config.getConfigId();
46+
}
47+
return provider + ":" + configId + ":" + voiceName;
4648
}
4749

4850
/**
4951
* 根据配置获取TTS服务
5052
*/
5153
public TtsService getTtsService(SysConfig config, String voiceName) {
54+
55+
config = !ObjectUtils.isEmpty(config) ? config : new SysConfig().setProvider(DEFAULT_PROVIDER);
56+
5257
// 如果提供商为空,则使用默认提供商
53-
var provider = ObjectUtils.isEmpty(config) ? DEFAULT_PROVIDER : config.getProvider();
54-
var cacheKey = createCacheKey(config, provider);
58+
var provider = config.getProvider();
59+
var cacheKey = createCacheKey(config, provider, voiceName);
5560

5661
// 检查是否已有该配置的服务实例
5762
if (serviceCache.containsKey(cacheKey)) {
5863
return serviceCache.get(cacheKey);
5964
}
6065

61-
var service = createApiService(config, voiceName, OUT_PUT_PATH);
66+
var service = createApiService(config, voiceName, OUTPUT_PATH);
6267
serviceCache.put(cacheKey, service);
6368
return service;
6469
}
@@ -79,16 +84,32 @@ private TtsService createApiService(SysConfig config, String voiceName, String o
7984
};
8085
}
8186

82-
public void removeCache(SysConfig config) {
83-
// 对于API服务,使用"provider:configId"作为缓存键,确保每个配置使用独立的服务实例
84-
Integer configId = config.getConfigId();
85-
String provider = config.getProvider();
86-
String cacheKey = provider + ":" + (configId != null ? configId : "default");
87-
serviceCache.remove(cacheKey);
88-
}
8987

9088
private void ensureOutputPath(String outputPath) {
9189
File dir = new File(outputPath);
9290
if (!dir.exists()) dir.mkdirs();
9391
}
92+
93+
public void removeCache(SysConfig config) {
94+
if (config == null) {
95+
return;
96+
}
97+
98+
String provider = config.getProvider();
99+
Integer configId = config.getConfigId();
100+
101+
// 遍历缓存的所有键,找到匹配的键并移除
102+
serviceCache.keySet().removeIf(key -> {
103+
String[] parts = key.split(":");
104+
if (parts.length != 3) {
105+
return false;
106+
}
107+
String keyProvider = parts[0];
108+
String keyConfigId = parts[1];
109+
110+
// 检查provider和configId是否匹配
111+
return keyProvider.equals(provider) && keyConfigId.equals(String.valueOf(configId));
112+
});
113+
114+
}
94115
}

src/main/java/com/xiaozhi/service/impl/SysDeviceServiceImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ public SysDevice update(SysDevice device) {
225225
@Override
226226
@Transactional(transactionManager = "transactionManager")
227227
public int updateNoRefreshCache(SysDevice device) {
228+
ChatSession session = sessionManager.getSessionByDeviceId(device.getDeviceId());
228229
if (!ObjectUtils.isEmpty(device.getRoleId())) {
229230
SysRole role = roleMapper.selectRoleById(device.getRoleId());
230231
if (role != null) {
@@ -238,6 +239,8 @@ public int updateNoRefreshCache(SysDevice device) {
238239
message.setDeviceId(device.getDeviceId());
239240
// 清空设备聊天记录
240241
messageMapper.delete(message);
242+
// TODO 后期切换时可以不用删除数据库中的记录,而是采用roleId来获取记忆内容
243+
session.setChatMemory(null);
241244
}
242245
}
243246
}

src/main/java/com/xiaozhi/utils/AudioUtils.java

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.io.InputStream;
1111
import java.nio.file.Files;
1212
import java.nio.file.Paths;
13+
import java.util.ArrayList;
1314
import java.util.List;
1415
import java.util.UUID;
1516

@@ -25,7 +26,7 @@ public class AudioUtils {
2526

2627
/**
2728
* 将原始音频数据保存为MP3文件
28-
*
29+
*
2930
* @param audio PCM音频数据
3031
* @return 文件名
3132
*/
@@ -102,9 +103,9 @@ public static String saveAsMp3(byte[] audio) {
102103
}
103104
}
104105

105-
/**
106+
/**
106107
* 将原始音频数据保存为WAV文件
107-
*
108+
*
108109
* @param audioData 音频数据
109110
* @return 文件名
110111
*/
@@ -118,9 +119,9 @@ public static String saveAsWav(byte[] audioData) {
118119
try {
119120
// 确保音频目录存在
120121
Files.createDirectories(Paths.get(AUDIO_PATH));
121-
122+
122123
try (FileOutputStream fos = new FileOutputStream(filePath);
123-
DataOutputStream dos = new DataOutputStream(fos)) {
124+
DataOutputStream dos = new DataOutputStream(fos)) {
124125

125126
// 写入WAV文件头
126127
// RIFF头
@@ -155,50 +156,43 @@ public static String saveAsWav(byte[] audioData) {
155156
}
156157

157158
/**
158-
* 合并多个WAV文件为一个WAV文件
159-
*
160-
* @param wavPaths 要合并的WAV文件路径列表
159+
* 合并多个音频文件为一个WAV文件
160+
* 支持合并的格式: wav, mp3, pcm
161+
*
162+
* @param audioPaths 要合并的音频文件路径列表
161163
* @return 合并后的WAV文件名
162164
*/
163-
public static String mergeWavFiles(List<String> wavPaths) {
164-
165-
if (wavPaths.size() == 1) {
166-
// 如果只有一个文件,直接返回该文件名
167-
String singlePath = wavPaths.get(0);
168-
if (singlePath.startsWith(AUDIO_PATH)) {
169-
return singlePath.substring(AUDIO_PATH.length());
170-
}
171-
return singlePath;
165+
public static String mergeAudioFiles(List<String> audioPaths) {
166+
if (audioPaths.size() == 1) {
167+
return Paths.get(audioPaths.getFirst()).getFileName().toString();
172168
}
173-
174-
String uuid = UUID.randomUUID().toString().replace("-", "");
175-
String outputFileName = uuid + ".wav";
176-
String outputPath = AUDIO_PATH + outputFileName;
177-
169+
var uuid = UUID.randomUUID().toString().replace("-", "");
170+
var outputFileName = uuid + ".wav";
171+
var outputPath = Paths.get(AUDIO_PATH, outputFileName).toString();
172+
178173
try {
179-
// 确保音频目录存在
180-
Files.createDirectories(Paths.get(AUDIO_PATH));
181-
182174
// 计算所有PCM数据的总大小
183-
long totalPcmSize = 0;
184-
for (String wavPath : wavPaths) {
185-
String fullPath = wavPath.startsWith(AUDIO_PATH) ? wavPath : AUDIO_PATH + wavPath;
186-
byte[] pcmData = wavToPcm(fullPath);
175+
var totalPcmSize = 0L;
176+
var audioChunks = new ArrayList<byte[]>();
177+
for (var audioPath : audioPaths) {
178+
var fullPath = audioPath.startsWith(AUDIO_PATH) ? audioPath : AUDIO_PATH + audioPath;
179+
byte[] pcmData = readAsPcm(fullPath);
187180
totalPcmSize += pcmData.length;
181+
audioChunks.add(pcmData);
188182
}
189-
183+
190184
// 创建输出WAV文件
191185
try (FileOutputStream fos = new FileOutputStream(outputPath);
192-
DataOutputStream dos = new DataOutputStream(fos)) {
193-
186+
DataOutputStream dos = new DataOutputStream(fos)) {
187+
194188
// 写入WAV文件头
195189
int bitsPerSample = 16; // 16位采样
196-
190+
197191
// RIFF头
198192
dos.writeBytes("RIFF");
199-
dos.writeInt(Integer.reverseBytes(36 + (int)totalPcmSize)); // 文件长度
193+
dos.writeInt(Integer.reverseBytes(36 + (int) totalPcmSize)); // 文件长度
200194
dos.writeBytes("WAVE");
201-
195+
202196
// fmt子块
203197
dos.writeBytes("fmt ");
204198
dos.writeInt(Integer.reverseBytes(16)); // 子块大小
@@ -208,29 +202,27 @@ public static String mergeWavFiles(List<String> wavPaths) {
208202
dos.writeInt(Integer.reverseBytes(SAMPLE_RATE * CHANNELS * bitsPerSample / 8)); // 字节率
209203
dos.writeShort(Short.reverseBytes((short) (CHANNELS * bitsPerSample / 8))); // 块对齐
210204
dos.writeShort(Short.reverseBytes((short) bitsPerSample)); // 每个样本的位数
211-
205+
212206
// data子块
213207
dos.writeBytes("data");
214-
dos.writeInt(Integer.reverseBytes((int)totalPcmSize)); // 数据大小
215-
208+
dos.writeInt(Integer.reverseBytes((int) totalPcmSize)); // 数据大小
209+
216210
// 依次写入每个文件的PCM数据
217-
for (String wavPath : wavPaths) {
218-
String fullPath = wavPath.startsWith(AUDIO_PATH) ? wavPath : AUDIO_PATH + wavPath;
219-
byte[] pcmData = wavToPcm(fullPath);
211+
for (var pcmData : audioChunks) {
220212
dos.write(pcmData);
221213
}
222214
}
223-
215+
224216
return outputFileName;
225217
} catch (Exception e) {
226-
logger.error("合并WAV文件时发生错误", e);
218+
logger.error("合并音频文件时发生错误", e);
227219
return null;
228220
}
229221
}
230222

231223
/**
232224
* 从WAV文件中提取PCM数据
233-
*
225+
*
234226
* @param wavPath WAV文件路径
235227
* @return PCM数据字节数组
236228
*/
@@ -242,7 +234,7 @@ public static byte[] wavToPcm(String wavPath) throws IOException {
242234

243235
/**
244236
* 从WAV字节数据中提取PCM数据
245-
*
237+
*
246238
* @param wavData WAV文件的字节数据
247239
* @return PCM数据字节数组
248240
*/
@@ -282,7 +274,7 @@ public static byte[] wavBytesToPcm(byte[] wavData) throws IOException {
282274

283275
/**
284276
* 从文件读取PCM数据,自动处理WAV和MP3格式
285-
*
277+
*
286278
* @param filePath 音频文件路径
287279
* @return PCM数据字节数组
288280
*/
@@ -301,7 +293,7 @@ public static byte[] readAsPcm(String filePath) throws IOException {
301293

302294
/**
303295
* 将MP3转换为PCM格式
304-
*
296+
*
305297
* @param mp3Path MP3文件路径
306298
* @return PCM数据字节数组
307299
*/
@@ -356,7 +348,7 @@ public static byte[] mp3ToPcm(String mp3Path) throws IOException {
356348

357349
/**
358350
* 检测音频文件格式并返回MIME类型
359-
*
351+
*
360352
* @param filePath 音频文件路径
361353
* @return MIME类型字符串
362354
*/

version.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=2.7.62
1+
version=2.7.64

0 commit comments

Comments
 (0)