Merge remote-tracking branch 'upstream/py_test_Memory_powermem' into add-powermem

# Conflicts:
#	main/xiaozhi-server/core/providers/memory/powermem/powermem.py
This commit is contained in:
渠磊
2026-01-09 12:32:59 +08:00
17 changed files with 193 additions and 104 deletions
@@ -23,11 +23,23 @@ public class VoiceDTO implements Serializable {
@Schema(description = "音频播放地址") @Schema(description = "音频播放地址")
private String voiceDemo; private String voiceDemo;
@Schema(description = "是否为克隆音色")
private Boolean isClone;
// 添加双参数构造函数,保持向后兼容 // 添加双参数构造函数,保持向后兼容
public VoiceDTO(String id, String name) { public VoiceDTO(String id, String name) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.voiceDemo = null; this.voiceDemo = null;
this.isClone = false; // 默认不是克隆音色
}
// 添加三参数构造函数,用于普通音色
public VoiceDTO(String id, String name, String voiceDemo) {
this.id = id;
this.name = name;
this.voiceDemo = voiceDemo;
this.isClone = false;
} }
} }
@@ -136,6 +136,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
.map(entity -> { .map(entity -> {
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName()); VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
dto.setVoiceDemo(entity.getVoiceDemo()); dto.setVoiceDemo(entity.getVoiceDemo());
dto.setIsClone(false); // 设置为普通音色
return dto; return dto;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -152,6 +153,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName()); voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName());
// 保留从数据库查询到的voiceDemo字段 // 保留从数据库查询到的voiceDemo字段
voiceDTO.setVoiceDemo(entity.getVoiceDemo()); voiceDTO.setVoiceDemo(entity.getVoiceDemo());
voiceDTO.setIsClone(true); // 设置为克隆音色
redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(), redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(),
RedisUtils.NOT_EXPIRE); RedisUtils.NOT_EXPIRE);
voiceDTOs.add(0, voiceDTO); voiceDTOs.add(0, voiceDTO);
@@ -214,6 +216,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
TimbreEntity entity = list.get(0); TimbreEntity entity = list.get(0);
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName()); VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
dto.setVoiceDemo(entity.getVoiceDemo()); dto.setVoiceDemo(entity.getVoiceDemo());
dto.setIsClone(false); // 设置为普通音色
return dto; return dto;
} }
} }
+14
View File
@@ -779,6 +779,16 @@ export default {
'roleConfig.fetchConfigFailed': 'Konfiguration konnte nicht abgerufen werden', 'roleConfig.fetchConfigFailed': 'Konfiguration konnte nicht abgerufen werden',
'roleConfig.fetchModelsFailed': 'Modellliste konnte nicht abgerufen werden', 'roleConfig.fetchModelsFailed': 'Modellliste konnte nicht abgerufen werden',
'roleConfig.fetchPluginsFailed': 'Plugin-Liste konnte nicht abgerufen werden', 'roleConfig.fetchPluginsFailed': 'Plugin-Liste konnte nicht abgerufen werden',
'roleConfig.cannotResumeAudio': 'Audio-Wiedergabe kann nicht fortgesetzt werden',
'roleConfig.selectVoiceFirst': 'Bitte wählen Sie zuerst eine Stimme aus',
'roleConfig.audioLoadTimeout': 'Audio-Laden dauert zu lange, bitte versuchen Sie es später erneut',
'roleConfig.cloneAudioPlayFailed': 'Klon-Audio-Wiedergabe fehlgeschlagen',
'roleConfig.cannotPlayCloneAudio': 'Klon-Audio kann nicht abgespielt werden',
'roleConfig.getCloneAudioFailed': 'Klon-Audio konnte nicht abgerufen werden',
'roleConfig.noPreviewAudio': 'Diese Stimme hat keine Vorschau-Audio-Datei',
'roleConfig.audioPlayFailed': 'Audio-Wiedergabe fehlgeschlagen',
'roleConfig.cannotPlayAudio': 'Audio kann nicht abgespielt werden',
'roleConfig.audioPlayError': 'Fehler bei der Audio-Wiedergabe',
// Function management dialog text // Function management dialog text
'functionDialog.title': 'Funktionsverwaltung', 'functionDialog.title': 'Funktionsverwaltung',
@@ -919,6 +929,10 @@ export default {
'ttsModel.selectVoiceToDelete': 'Bitte wählen Sie Stimmen zum Löschen aus', 'ttsModel.selectVoiceToDelete': 'Bitte wählen Sie Stimmen zum Löschen aus',
'ttsModel.warning': 'Warnung', 'ttsModel.warning': 'Warnung',
'ttsModel.confirmDeleteVoice': 'Sind Sie sicher, dass Sie {count} Stimmen löschen möchten?', 'ttsModel.confirmDeleteVoice': 'Sind Sie sicher, dass Sie {count} Stimmen löschen möchten?',
'ttsModel.voiceCodeNameLanguageRequired': 'Stimmcode, Stimmaname und Sprache können nicht leer sein',
'ttsModel.deleteCancelled': 'Löschvorgang abgebrochen',
'ttsModel.updateSuccess': 'Aktualisierung erfolgreich',
'ttsModel.saveSuccess': 'Speicherung erfolgreich',
// OTA Management Page Text // OTA Management Page Text
'otaManagement.firmwareManagement': 'Firmware-Verwaltung', 'otaManagement.firmwareManagement': 'Firmware-Verwaltung',
+14
View File
@@ -779,6 +779,16 @@ export default {
'roleConfig.fetchConfigFailed': 'Failed to fetch configuration', 'roleConfig.fetchConfigFailed': 'Failed to fetch configuration',
'roleConfig.fetchModelsFailed': 'Failed to fetch model list', 'roleConfig.fetchModelsFailed': 'Failed to fetch model list',
'roleConfig.fetchPluginsFailed': 'Failed to fetch plugin list', 'roleConfig.fetchPluginsFailed': 'Failed to fetch plugin list',
'roleConfig.cannotResumeAudio': 'Cannot resume audio playback',
'roleConfig.selectVoiceFirst': 'Please select a voice first',
'roleConfig.audioLoadTimeout': 'Audio loading takes too long, please try again later',
'roleConfig.cloneAudioPlayFailed': 'Clone audio playback failed',
'roleConfig.cannotPlayCloneAudio': 'Cannot play clone audio',
'roleConfig.getCloneAudioFailed': 'Failed to get clone audio',
'roleConfig.noPreviewAudio': 'This voice has no preview audio available',
'roleConfig.audioPlayFailed': 'Audio playback failed',
'roleConfig.cannotPlayAudio': 'Cannot play audio',
'roleConfig.audioPlayError': 'Error occurred during audio playback',
// Function management dialog text // Function management dialog text
'functionDialog.title': 'Function Management', 'functionDialog.title': 'Function Management',
@@ -919,6 +929,10 @@ export default {
'ttsModel.selectVoiceToDelete': 'Please select voices to delete', 'ttsModel.selectVoiceToDelete': 'Please select voices to delete',
'ttsModel.warning': 'Warning', 'ttsModel.warning': 'Warning',
'ttsModel.confirmDeleteVoice': 'Are you sure to delete {count} voices?', 'ttsModel.confirmDeleteVoice': 'Are you sure to delete {count} voices?',
'ttsModel.voiceCodeNameLanguageRequired': 'Voice code, voice name and language type cannot be empty',
'ttsModel.deleteCancelled': 'Deletion cancelled',
'ttsModel.updateSuccess': 'Update successful',
'ttsModel.saveSuccess': 'Save successful',
// OTA Management Page Text // OTA Management Page Text
'otaManagement.firmwareManagement': 'Firmware Management', 'otaManagement.firmwareManagement': 'Firmware Management',
+14
View File
@@ -779,6 +779,16 @@ export default {
'roleConfig.fetchConfigFailed': 'Không thể lấy cấu hình', 'roleConfig.fetchConfigFailed': 'Không thể lấy cấu hình',
'roleConfig.fetchModelsFailed': 'Không thể tải danh sách mô hình', 'roleConfig.fetchModelsFailed': 'Không thể tải danh sách mô hình',
'roleConfig.fetchPluginsFailed': 'Không thể tải danh sách plugin', 'roleConfig.fetchPluginsFailed': 'Không thể tải danh sách plugin',
'roleConfig.cannotResumeAudio': 'Không thể tiếp tục phát âm thanh',
'roleConfig.selectVoiceFirst': 'Vui lòng chọn giọng nói trước',
'roleConfig.audioLoadTimeout': 'Tải âm thanh mất quá nhiều thời gian, vui lòng thử lại sau',
'roleConfig.cloneAudioPlayFailed': 'Phát âm thanh sao chép thất bại',
'roleConfig.cannotPlayCloneAudio': 'Không thể phát âm thanh sao chép',
'roleConfig.getCloneAudioFailed': 'Không thể lấy âm thanh sao chép',
'roleConfig.noPreviewAudio': 'Giọng nói này không có âm thanh xem trước',
'roleConfig.audioPlayFailed': 'Phát âm thanh thất bại',
'roleConfig.cannotPlayAudio': 'Không thể phát âm thanh',
'roleConfig.audioPlayError': 'Lỗi trong quá trình phát âm thanh',
// Function management dialog text // Function management dialog text
'functionDialog.title': 'Quản lý chức năng', 'functionDialog.title': 'Quản lý chức năng',
@@ -919,6 +929,10 @@ export default {
'ttsModel.selectVoiceToDelete': 'Vui lòng chọn giọng nói để xóa', 'ttsModel.selectVoiceToDelete': 'Vui lòng chọn giọng nói để xóa',
'ttsModel.warning': 'Cảnh báo', 'ttsModel.warning': 'Cảnh báo',
'ttsModel.confirmDeleteVoice': 'Bạn có chắc chắn muốn xóa {count} giọng nói?', 'ttsModel.confirmDeleteVoice': 'Bạn có chắc chắn muốn xóa {count} giọng nói?',
'ttsModel.voiceCodeNameLanguageRequired': 'Mã giọng nói, tên giọng nói và loại ngôn ngữ không được để trống',
'ttsModel.deleteCancelled': 'Xóa đã bị hủy',
'ttsModel.updateSuccess': 'Cập nhật thành công',
'ttsModel.saveSuccess': 'Lưu thành công',
// OTA Management Page Text // OTA Management Page Text
'otaManagement.firmwareManagement': 'Quản lý firmware', 'otaManagement.firmwareManagement': 'Quản lý firmware',
+14
View File
@@ -779,6 +779,16 @@ export default {
'roleConfig.fetchConfigFailed': '获取配置失败', 'roleConfig.fetchConfigFailed': '获取配置失败',
'roleConfig.fetchModelsFailed': '获取模型列表失败', 'roleConfig.fetchModelsFailed': '获取模型列表失败',
'roleConfig.fetchPluginsFailed': '获取插件列表失败', 'roleConfig.fetchPluginsFailed': '获取插件列表失败',
'roleConfig.cannotResumeAudio': '无法恢复播放音频',
'roleConfig.selectVoiceFirst': '请先选择一个音色',
'roleConfig.audioLoadTimeout': '音频加载时间较长,请稍后重试',
'roleConfig.cloneAudioPlayFailed': '克隆音频播放失败',
'roleConfig.cannotPlayCloneAudio': '无法播放克隆音频',
'roleConfig.getCloneAudioFailed': '获取克隆音频失败',
'roleConfig.noPreviewAudio': '该音色暂无可预览的音频',
'roleConfig.audioPlayFailed': '音频播放失败',
'roleConfig.cannotPlayAudio': '无法播放音频',
'roleConfig.audioPlayError': '播放音频过程出错',
// 功能管理对话框文本 // 功能管理对话框文本
'functionDialog.title': '功能管理', 'functionDialog.title': '功能管理',
@@ -919,6 +929,10 @@ export default {
'ttsModel.selectVoiceToDelete': '请选择要删除的音色', 'ttsModel.selectVoiceToDelete': '请选择要删除的音色',
'ttsModel.warning': '警告', 'ttsModel.warning': '警告',
'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?', 'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?',
'ttsModel.voiceCodeNameLanguageRequired': '音色编码、音色名称和语言类型不能为空',
'ttsModel.deleteCancelled': '删除已取消',
'ttsModel.updateSuccess': '更新成功',
'ttsModel.saveSuccess': '保存成功',
// OTA管理页面文本 // OTA管理页面文本
'otaManagement.firmwareManagement': '固件管理', 'otaManagement.firmwareManagement': '固件管理',
+14
View File
@@ -779,6 +779,16 @@ export default {
'roleConfig.fetchConfigFailed': '獲取配置失敗', 'roleConfig.fetchConfigFailed': '獲取配置失敗',
'roleConfig.fetchModelsFailed': '獲取模型列表失敗', 'roleConfig.fetchModelsFailed': '獲取模型列表失敗',
'roleConfig.fetchPluginsFailed': '獲取插件列表失敗', 'roleConfig.fetchPluginsFailed': '獲取插件列表失敗',
'roleConfig.cannotResumeAudio': '無法恢復播放音訊',
'roleConfig.selectVoiceFirst': '請先選擇一個音色',
'roleConfig.audioLoadTimeout': '音訊載入時間較長,請稍後重試',
'roleConfig.cloneAudioPlayFailed': '克隆音訊播放失敗',
'roleConfig.cannotPlayCloneAudio': '無法播放克隆音訊',
'roleConfig.getCloneAudioFailed': '獲取克隆音訊失敗',
'roleConfig.noPreviewAudio': '該音色暫無可預覽的音訊',
'roleConfig.audioPlayFailed': '音訊播放失敗',
'roleConfig.cannotPlayAudio': '無法播放音訊',
'roleConfig.audioPlayError': '播放音訊過程出錯',
// 功能管理對話框文本 // 功能管理對話框文本
'functionDialog.title': '功能管理', 'functionDialog.title': '功能管理',
@@ -917,6 +927,10 @@ export default {
'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色', 'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色',
'ttsModel.warning': '警告', 'ttsModel.warning': '警告',
'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?', 'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?',
'ttsModel.voiceCodeNameLanguageRequired': '音色編碼、音色名稱和語言類型不能為空',
'ttsModel.deleteCancelled': '刪除已取消',
'ttsModel.updateSuccess': '更新成功',
'ttsModel.saveSuccess': '保存成功',
'ttsModel.operationFailed': '操作失敗', 'ttsModel.operationFailed': '操作失敗',
'ttsModel.operationClosed': '操作已關閉', 'ttsModel.operationClosed': '操作已關閉',
+26 -55
View File
@@ -599,21 +599,15 @@ export default {
this.voiceOptions = data.data.map((voice) => ({ this.voiceOptions = data.data.map((voice) => ({
value: voice.id, value: voice.id,
label: voice.name, label: voice.name,
// 复制音频相关字段,确保hasAudioPreview能检测到 // 只保留后端实际返回的音频相关字段
voiceDemo: voice.voiceDemo, voiceDemo: voice.voiceDemo,
demoUrl: voice.demoUrl,
audioUrl: voice.audioUrl,
voice_demo: voice.voice_demo, voice_demo: voice.voice_demo,
sample_voice: voice.sample_voice, // 使用后端实际返回的 isClone 字段
referenceAudio: voice.referenceAudio, isClone: Boolean(voice.isClone),
// 新增:添加克隆音频相关字段 // 保存训练状态字段
cloneAudioUrl: voice.cloneAudioUrl,
hasCloneAudio: voice.hasCloneAudio || false,
// 保存训练状态字段,用于判断是否为克隆音频
train_status: voice.trainStatus, train_status: voice.trainStatus,
})); }));
// 保存完整的音色信息,添加调试信息 // 保存完整的音色信息,添加调试信息
console.log("获取到的音色数据:", data.data);
this.voiceDetails = data.data.reduce((acc, voice) => { this.voiceDetails = data.data.reduce((acc, voice) => {
acc[voice.id] = voice; acc[voice.id] = voice;
return acc; return acc;
@@ -746,32 +740,15 @@ export default {
}, },
// 检查是否有音频预览 // 检查是否有音频预览
hasAudioPreview(item) { hasAudioPreview(item) {
// 检查item中是否包含有效的音频URL字段或克隆音频字段 // 检查是否为克隆音频
// 克隆音频通过hasCloneAudio标志或ID格式判断(非TTS开头的ID) // 使用后端实际返回的 isClone 字段
const isCloneAudio = const isCloneAudio = Boolean(item.isClone);
item.hasCloneAudio || (item.value && !item.value.startsWith("TTS"));
const audioFields = [ // 检查是否有有效的音频URL,只使用后端实际返回的字段
item.voiceDemo, const hasValidAudioUrl = !!((item.voice_demo || item.voiceDemo)?.trim());
item.demoUrl,
item.audioUrl,
item.voice_demo,
item.sample_voice,
item.referenceAudio,
item.cloneAudioUrl, // 克隆音频的URL
];
// 检查是否有任何音频字段是有效URL // 克隆音频始终显示播放按钮,普通音频需要有有效URL才显示
const hasUrlAudio = audioFields.some( return isCloneAudio || hasValidAudioUrl;
(field) =>
field !== undefined &&
field !== null &&
typeof field === "string" &&
field.trim() !== "" &&
field.toLowerCase().startsWith("http")
);
return hasUrlAudio || isCloneAudio;
}, },
// 播放/暂停音频切换 // 播放/暂停音频切换
@@ -782,7 +759,7 @@ export default {
// 从暂停状态恢复播放 // 从暂停状态恢复播放
this.currentAudio.play().catch((error) => { this.currentAudio.play().catch((error) => {
console.error("恢复播放失败:", error); console.error("恢复播放失败:", error);
this.$message.warning("无法恢复播放音频"); this.$message.warning(this.$t('roleConfig.cannotResumeAudio'));
}); });
this.isPaused = false; this.isPaused = false;
} else { } else {
@@ -803,7 +780,7 @@ export default {
const targetVoiceId = voiceId || this.form.ttsVoiceId; const targetVoiceId = voiceId || this.form.ttsVoiceId;
if (!targetVoiceId) { if (!targetVoiceId) {
this.$message.warning("请先选择一个音色"); this.$message.warning(this.$t('roleConfig.selectVoiceFirst'));
return; return;
} }
@@ -830,10 +807,8 @@ export default {
let isCloneAudio = false; let isCloneAudio = false;
if (voiceDetail) { if (voiceDetail) {
// 首先检查是否克隆音频(通过ID格式判断,非TTS开头的ID) // 使用后端实际返回的 isClone 字段判断是否克隆音频
isCloneAudio = isCloneAudio = Boolean(voiceDetail.isClone);
voiceDetail.hasCloneAudio ||
(voiceDetail.id && !voiceDetail.id.startsWith("TTS"));
console.log( console.log(
"克隆音频判断结果:", "克隆音频判断结果:",
isCloneAudio, isCloneAudio,
@@ -889,7 +864,7 @@ export default {
// 设置超时,防止加载过长时间 // 设置超时,防止加载过长时间
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
if (this.currentAudio && this.playingVoice) { if (this.currentAudio && this.playingVoice) {
this.$message.warning("音频加载时间较长,请稍后重试"); this.$message.warning(this.$t('roleConfig.audioLoadTimeout'));
this.playingVoice = false; this.playingVoice = false;
} }
}, 10000); // 10秒超时 }, 10000); // 10秒超时
@@ -898,7 +873,7 @@ export default {
this.currentAudio.onerror = () => { this.currentAudio.onerror = () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.error("克隆音频播放错误"); console.error("克隆音频播放错误");
this.$message.warning("克隆音频播放失败"); this.$message.warning(this.$t('roleConfig.cloneAudioPlayFailed'));
this.playingVoice = false; this.playingVoice = false;
}; };
@@ -920,12 +895,12 @@ export default {
this.currentAudio.play().catch((error) => { this.currentAudio.play().catch((error) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.error("播放克隆音频失败:", error); console.error("播放克隆音频失败:", error);
this.$message.warning("无法播放克隆音频"); this.$message.warning(this.$t('roleConfig.cannotPlayCloneAudio'));
this.playingVoice = false; this.playingVoice = false;
}); });
} else { } else {
clearTimeout(timeoutId); clearTimeout(timeoutId);
this.$message.warning("获取克隆音频失败"); this.$message.warning(this.$t('roleConfig.getCloneAudioFailed'));
this.playingVoice = false; this.playingVoice = false;
} }
}); });
@@ -933,14 +908,10 @@ export default {
// 返回,避免继续执行下面的普通音频播放逻辑 // 返回,避免继续执行下面的普通音频播放逻辑
return; return;
} else { } else {
// 对于普通音频,尝试各种可能的URL字段 // 对于普通音频,只使用后端实际返回的字段
audioUrl = audioUrl =
voiceDetail.voiceDemo || voiceDetail.voiceDemo ||
voiceDetail.demoUrl || voiceDetail.voice_demo;
voiceDetail.audioUrl ||
voiceDetail.voice_demo ||
voiceDetail.sample_voice ||
voiceDetail.cloneAudioUrl; // 克隆音频URL
} }
// 如果没有找到,尝试检查是否有URL格式的字段 // 如果没有找到,尝试检查是否有URL格式的字段
@@ -965,7 +936,7 @@ export default {
if (!audioUrl) { if (!audioUrl) {
// 如果没有音频URL显示友好的提示 // 如果没有音频URL显示友好的提示
this.$message.warning("该音色暂无可预览的音频"); this.$message.warning(this.$t('roleConfig.noPreviewAudio'));
return; return;
} }
@@ -984,7 +955,7 @@ export default {
// 设置超时防止加载过长时间 // 设置超时防止加载过长时间
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
if (this.currentAudio && this.playingVoice) { if (this.currentAudio && this.playingVoice) {
this.$message.warning("音频加载时间较长,请稍后重试"); this.$message.warning(this.$t('roleConfig.audioLoadTimeout'));
this.playingVoice = false; this.playingVoice = false;
} }
}, 10000); // 10秒超时 }, 10000); // 10秒超时
@@ -993,7 +964,7 @@ export default {
this.currentAudio.onerror = () => { this.currentAudio.onerror = () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.error("音频播放错误"); console.error("音频播放错误");
this.$message.warning("音频播放失败"); this.$message.warning(this.$t('roleConfig.audioPlayFailed'));
this.playingVoice = false; this.playingVoice = false;
}; };
@@ -1011,13 +982,13 @@ export default {
this.currentAudio.play().catch((error) => { this.currentAudio.play().catch((error) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.error("播放失败:", error); console.error("播放失败:", error);
this.$message.warning("无法播放音频"); this.$message.warning(this.$t('roleConfig.cannotPlayAudio'));
this.playingVoice = false; this.playingVoice = false;
}); });
} }
} catch (error) { } catch (error) {
console.error("播放音频过程出错:", error); console.error("播放音频过程出错:", error);
this.$message.error("播放音频过程出错"); this.$message.error(this.$t('roleConfig.audioPlayError'));
this.playingVoice = false; this.playingVoice = false;
} }
}, },
@@ -1,5 +1,6 @@
import time import time
import json import json
import uuid
import random import random
import asyncio import asyncio
from core.utils.dialogue import Message from core.utils.dialogue import Message
@@ -105,6 +106,9 @@ async def checkWakeupWords(conn, text):
# 播放唤醒词回复 # 播放唤醒词回复
conn.client_abort = False conn.client_abort = False
# 将唤醒词回复视为新会话,生成新的 sentence_id,确保流控器重置
conn.sentence_id = str(uuid.uuid4().hex)
conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response.get('text')}") conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response.get('text')}")
await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response.get("text")) await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response.get("text"))
await sendAudioMessage(conn, SentenceType.LAST, [], None) await sendAudioMessage(conn, SentenceType.LAST, [], None)
@@ -133,12 +133,27 @@ def _get_or_create_rate_controller(conn, frame_duration, is_single_packet):
Returns: Returns:
(rate_controller, flow_control) (rate_controller, flow_control)
""" """
# 判断是否需要重置:单包模式且 sentence_id 变化,或者控制器不存在 # 检查是否需要重置控制器
need_reset = ( need_reset = False
is_single_packet
and getattr(conn, "audio_flow_control", {}).get("sentence_id") if not hasattr(conn, "audio_rate_controller"):
!= conn.sentence_id # 控制器不存在,需要创建
) or not hasattr(conn, "audio_rate_controller") need_reset = True
else:
rate_controller = conn.audio_rate_controller
# 后台发送任务已停止, 则需要重置
if (
not rate_controller.pending_send_task
or rate_controller.pending_send_task.done()
):
need_reset = True
# 当sentence_id 变化,需要重置
elif (
getattr(conn, "audio_flow_control", {}).get("sentence_id")
!= conn.sentence_id
):
need_reset = True
if need_reset: if need_reset:
# 创建或获取 rate_controller # 创建或获取 rate_controller
@@ -150,13 +150,15 @@ class MemoryProvider(MemoryProviderBase):
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}") logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
self.use_powermem = False self.use_powermem = False
async def save_memory(self, msgs): async def save_memory(self, msgs, session_id=None):
""" """
Save conversation messages to PowerMem. Save conversation messages to PowerMem.
Args: Args:
msgs: List of message objects with 'role' and 'content' attributes msgs: List of message objects with 'role' and 'content' attributes
session_id: Session identifier (optional, for compatibility)
Returns: Returns:
Result from PowerMem API or None if failed Result from PowerMem API or None if failed
""" """
@@ -306,3 +308,6 @@ class MemoryProvider(MemoryProviderBase):
return "" return ""
# Register the memory provider instance
powermem = MemoryProvider({})
@@ -185,20 +185,21 @@ class TTSProvider(TTSProviderBase):
# 过滤Markdown # 过滤Markdown
filtered_text = MarkdownCleaner.clean_markdown(text) filtered_text = MarkdownCleaner.clean_markdown(text)
# 发送continue-task消息 if filtered_text:
continue_task_message = { # 发送continue-task消息
"header": { continue_task_message = {
"action": "continue-task", "header": {
"task_id": self.conn.sentence_id, "action": "continue-task",
"streaming": "duplex", "task_id": self.conn.sentence_id,
}, "streaming": "duplex",
"payload": {"input": {"text": filtered_text}}, },
} "payload": {"input": {"text": filtered_text}},
}
await self.ws.send(json.dumps(continue_task_message))
self.last_active_time = time.time()
logger.bind(tag=TAG).debug(f"已发送文本: {filtered_text}")
await self.ws.send(json.dumps(continue_task_message))
self.last_active_time = time.time()
logger.bind(tag=TAG).debug(f"已发送文本: {filtered_text}")
return
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
if self.ws: if self.ws:
@@ -288,18 +288,19 @@ class TTSProvider(TTSProviderBase):
logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本") logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
return return
filtered_text = MarkdownCleaner.clean_markdown(text) filtered_text = MarkdownCleaner.clean_markdown(text)
run_request = { if filtered_text:
"header": { run_request = {
"message_id": uuid.uuid4().hex, "header": {
"task_id": self.task_id, "message_id": uuid.uuid4().hex,
"namespace": "FlowingSpeechSynthesizer", "task_id": self.task_id,
"name": "RunSynthesis", "namespace": "FlowingSpeechSynthesizer",
"appkey": self.appkey, "name": "RunSynthesis",
}, "appkey": self.appkey,
"payload": {"text": filtered_text}, },
} "payload": {"text": filtered_text},
await self.ws.send(json.dumps(run_request)) }
self.last_active_time = time.time() await self.ws.send(json.dumps(run_request))
self.last_active_time = time.time()
return return
except Exception as e: except Exception as e:
@@ -4,15 +4,15 @@ import json
import queue import queue
import asyncio import asyncio
import traceback import traceback
from typing import Callable, Any
import websockets import websockets
from typing import Callable, Any
from core.utils.tts import MarkdownCleaner from core.utils.tts import MarkdownCleaner
from config.logger import setup_logging from config.logger import setup_logging
from core.utils import opus_encoder_utils from core.utils import opus_encoder_utils
from core.utils.util import check_model_key from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
from asyncio import Task
TAG = __name__ TAG = __name__
@@ -340,8 +340,9 @@ class TTSProvider(TTSProviderBase):
# 过滤Markdown # 过滤Markdown
filtered_text = MarkdownCleaner.clean_markdown(text) filtered_text = MarkdownCleaner.clean_markdown(text)
# 发送文本 if filtered_text:
await self.send_text(self.voice, filtered_text, self.conn.sentence_id) # 发送文本
await self.send_text(self.voice, filtered_text, self.conn.sentence_id)
return return
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
@@ -237,10 +237,10 @@ class TTSProvider(TTSProviderBase):
return return
filtered_text = MarkdownCleaner.clean_markdown(text) filtered_text = MarkdownCleaner.clean_markdown(text)
if filtered_text:
# 发送文本合成请求 # 发送文本合成请求
run_request = self._build_base_request(status=1,text=filtered_text) run_request = self._build_base_request(status=1,text=filtered_text)
await self.ws.send(json.dumps(run_request)) await self.ws.send(json.dumps(run_request))
return return
except Exception as e: except Exception as e:
+1 -1
View File
@@ -2,7 +2,7 @@ import json
TAG = __name__ TAG = __name__
EMOJI_MAP = { EMOJI_MAP = {
"😂": "laughing", "😂": "funny",
"😭": "crying", "😭": "crying",
"😠": "angry", "😠": "angry",
"😔": "sad", "😔": "sad",
+7 -1
View File
@@ -1,9 +1,11 @@
import os import os
import re import re
import sys import sys
from config.logger import setup_logging
import importlib import importlib
from config.logger import setup_logging
from core.utils.textUtils import check_emoji
logger = setup_logging() logger = setup_logging()
punctuation_set = { punctuation_set = {
@@ -135,4 +137,8 @@ class MarkdownCleaner:
for regex, replacement in MarkdownCleaner.REGEXES: for regex, replacement in MarkdownCleaner.REGEXES:
text = regex.sub(replacement, text) text = regex.sub(replacement, text)
# 去除emoji表情
text = check_emoji(text)
return text.strip() return text.strip()