From bcb51729a5452bc5404ab8004b9fbf457334e9a2 Mon Sep 17 00:00:00 2001 From: LJH-rgsze <3241304429@qq.com> Date: Wed, 7 Jan 2026 16:00:35 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=9A=84=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xiaozhi/modules/model/dto/VoiceDTO.java | 12 ++++++ .../service/impl/TimbreServiceImpl.java | 3 ++ main/manager-web/src/views/roleConfig.vue | 37 ++++++------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java index 4db614ef..e69608a9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java @@ -22,12 +22,24 @@ public class VoiceDTO implements Serializable { @Schema(description = "音频播放地址") private String voiceDemo; + + @Schema(description = "是否为克隆音色") + private Boolean isClone; // 添加双参数构造函数,保持向后兼容 public VoiceDTO(String id, String name) { this.id = id; this.name = name; 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; } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java index feb6c449..9d6ca5c4 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java @@ -136,6 +136,7 @@ public class TimbreServiceImpl extends BaseServiceImpl .map(entity -> { VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName()); dto.setVoiceDemo(entity.getVoiceDemo()); + dto.setIsClone(false); // 设置为普通音色 return dto; }) .collect(Collectors.toList()); @@ -152,6 +153,7 @@ public class TimbreServiceImpl extends BaseServiceImpl voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName()); // 保留从数据库查询到的voiceDemo字段 voiceDTO.setVoiceDemo(entity.getVoiceDemo()); + voiceDTO.setIsClone(true); // 设置为克隆音色 redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(), RedisUtils.NOT_EXPIRE); voiceDTOs.add(0, voiceDTO); @@ -214,6 +216,7 @@ public class TimbreServiceImpl extends BaseServiceImpl TimbreEntity entity = list.get(0); VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName()); dto.setVoiceDemo(entity.getVoiceDemo()); + dto.setIsClone(false); // 设置为普通音色 return dto; } } \ No newline at end of file diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index 4203ceb9..f753b868 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -746,32 +746,17 @@ export default { }, // 检查是否有音频预览 hasAudioPreview(item) { - // 检查item中是否包含有效的音频URL字段或克隆音频字段 - // 克隆音频通过hasCloneAudio标志或ID格式判断(非TTS开头的ID) - const isCloneAudio = - item.hasCloneAudio || (item.value && !item.value.startsWith("TTS")); - - const audioFields = [ - item.voiceDemo, - item.demoUrl, - item.audioUrl, - item.voice_demo, - item.sample_voice, - item.referenceAudio, - item.cloneAudioUrl, // 克隆音频的URL - ]; - - // 检查是否有任何音频字段是有效的URL - const hasUrlAudio = audioFields.some( - (field) => - field !== undefined && - field !== null && - typeof field === "string" && - field.trim() !== "" && - field.toLowerCase().startsWith("http") - ); - - return hasUrlAudio || isCloneAudio; + // 使用新增的isClone字段来判断是否为克隆音色 + const isCloneAudio = item.isClone === true; + + // 普通音色的音频检查:同时检查voiceDemo和voice_demo两种命名格式 + const audioField = item.voice_demo || item.voiceDemo; + const hasManualAudio = audioField && + typeof audioField === "string" && + audioField.trim() !== "" && + audioField.toLowerCase().startsWith("http"); + + return isCloneAudio || hasManualAudio; }, // 播放/暂停音频切换 From 81e923b4f713f4703578830ca3ad5c388e7ba3bf Mon Sep 17 00:00:00 2001 From: LJH-rgsze <3241304429@qq.com> Date: Wed, 7 Jan 2026 16:52:36 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E8=A1=A5=E5=85=85=E9=9F=B3=E8=89=B2?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E7=BC=BA=E5=A4=B1=E7=9A=84?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/de.js | 4 ++++ main/manager-web/src/i18n/en.js | 4 ++++ main/manager-web/src/i18n/vi.js | 4 ++++ main/manager-web/src/i18n/zh_CN.js | 4 ++++ main/manager-web/src/i18n/zh_TW.js | 2 ++ 5 files changed, 18 insertions(+) diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index 22512e8c..81bb1b5d 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -919,6 +919,10 @@ export default { 'ttsModel.selectVoiceToDelete': 'Bitte wählen Sie Stimmen zum Löschen aus', 'ttsModel.warning': 'Warnung', '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 'otaManagement.firmwareManagement': 'Firmware-Verwaltung', diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 9bcc1f7d..3314adca 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -919,6 +919,10 @@ export default { 'ttsModel.selectVoiceToDelete': 'Please select voices to delete', 'ttsModel.warning': 'Warning', '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 'otaManagement.firmwareManagement': 'Firmware Management', diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index c9d9e1cf..9191233a 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -919,6 +919,10 @@ export default { 'ttsModel.selectVoiceToDelete': 'Vui lòng chọn giọng nói để xóa', '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.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 'otaManagement.firmwareManagement': 'Quản lý firmware', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 44d8e400..a8439c62 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -919,6 +919,10 @@ export default { 'ttsModel.selectVoiceToDelete': '请选择要删除的音色', 'ttsModel.warning': '警告', 'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?', + 'ttsModel.voiceCodeNameLanguageRequired': '音色编码、音色名称和语言类型不能为空', + 'ttsModel.deleteCancelled': '删除已取消', + 'ttsModel.updateSuccess': '更新成功', + 'ttsModel.saveSuccess': '保存成功', // OTA管理页面文本 'otaManagement.firmwareManagement': '固件管理', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index ca5a5221..b4089c09 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -917,6 +917,8 @@ export default { 'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色', 'ttsModel.warning': '警告', 'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?', + 'ttsModel.voiceCodeNameLanguageRequired': '音色編碼、音色名稱和語言類型不能為空', + 'ttsModel.deleteCancelled': '刪除已取消', 'ttsModel.operationFailed': '操作失敗', 'ttsModel.operationClosed': '操作已關閉', From 07d5b3cd2a4676598c4f68a3d923e53f4dc650b6 Mon Sep 17 00:00:00 2001 From: LJH-rgsze <3241304429@qq.com> Date: Wed, 7 Jan 2026 16:54:08 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E8=A1=A5=E5=85=85-=E4=B8=8A=E4=B8=AA?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=8A=E4=BC=A0=E5=B0=91=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/zh_TW.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index b4089c09..20c89ceb 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -919,6 +919,8 @@ export default { 'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?', 'ttsModel.voiceCodeNameLanguageRequired': '音色編碼、音色名稱和語言類型不能為空', 'ttsModel.deleteCancelled': '刪除已取消', + 'ttsModel.updateSuccess': '更新成功', + 'ttsModel.saveSuccess': '保存成功', 'ttsModel.operationFailed': '操作失敗', 'ttsModel.operationClosed': '操作已關閉', From c190abe4b14676dc2ae9f6a451a9d9afff82a9ac Mon Sep 17 00:00:00 2001 From: LJH-rgsze <3241304429@qq.com> Date: Wed, 7 Jan 2026 17:03:56 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E7=94=A8=E5=9B=BD=E9=99=85=E5=8C=96?= =?UTF-8?q?=E9=87=8D=E5=86=99=E7=A1=AC=E7=BC=96=E7=A0=81=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/de.js | 10 ++++++++++ main/manager-web/src/i18n/en.js | 10 ++++++++++ main/manager-web/src/i18n/vi.js | 10 ++++++++++ main/manager-web/src/i18n/zh_CN.js | 10 ++++++++++ main/manager-web/src/i18n/zh_TW.js | 10 ++++++++++ main/manager-web/src/views/roleConfig.vue | 22 +++++++++++----------- 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index 81bb1b5d..c726e497 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -779,6 +779,16 @@ export default { 'roleConfig.fetchConfigFailed': 'Konfiguration konnte nicht abgerufen werden', 'roleConfig.fetchModelsFailed': 'Modellliste 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 'functionDialog.title': 'Funktionsverwaltung', diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 3314adca..dd5ab2ff 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -779,6 +779,16 @@ export default { 'roleConfig.fetchConfigFailed': 'Failed to fetch configuration', 'roleConfig.fetchModelsFailed': 'Failed to fetch model 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 'functionDialog.title': 'Function Management', diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index 9191233a..5a73b968 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -779,6 +779,16 @@ export default { '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.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 'functionDialog.title': 'Quản lý chức năng', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index a8439c62..7452808b 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -779,6 +779,16 @@ export default { 'roleConfig.fetchConfigFailed': '获取配置失败', 'roleConfig.fetchModelsFailed': '获取模型列表失败', 'roleConfig.fetchPluginsFailed': '获取插件列表失败', + 'roleConfig.cannotResumeAudio': '无法恢复播放音频', + 'roleConfig.selectVoiceFirst': '请先选择一个音色', + 'roleConfig.audioLoadTimeout': '音频加载时间较长,请稍后重试', + 'roleConfig.cloneAudioPlayFailed': '克隆音频播放失败', + 'roleConfig.cannotPlayCloneAudio': '无法播放克隆音频', + 'roleConfig.getCloneAudioFailed': '获取克隆音频失败', + 'roleConfig.noPreviewAudio': '该音色暂无可预览的音频', + 'roleConfig.audioPlayFailed': '音频播放失败', + 'roleConfig.cannotPlayAudio': '无法播放音频', + 'roleConfig.audioPlayError': '播放音频过程出错', // 功能管理对话框文本 'functionDialog.title': '功能管理', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 20c89ceb..729d7151 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -779,6 +779,16 @@ export default { 'roleConfig.fetchConfigFailed': '獲取配置失敗', 'roleConfig.fetchModelsFailed': '獲取模型列表失敗', 'roleConfig.fetchPluginsFailed': '獲取插件列表失敗', + 'roleConfig.cannotResumeAudio': '無法恢復播放音訊', + 'roleConfig.selectVoiceFirst': '請先選擇一個音色', + 'roleConfig.audioLoadTimeout': '音訊載入時間較長,請稍後重試', + 'roleConfig.cloneAudioPlayFailed': '克隆音訊播放失敗', + 'roleConfig.cannotPlayCloneAudio': '無法播放克隆音訊', + 'roleConfig.getCloneAudioFailed': '獲取克隆音訊失敗', + 'roleConfig.noPreviewAudio': '該音色暫無可預覽的音訊', + 'roleConfig.audioPlayFailed': '音訊播放失敗', + 'roleConfig.cannotPlayAudio': '無法播放音訊', + 'roleConfig.audioPlayError': '播放音訊過程出錯', // 功能管理對話框文本 'functionDialog.title': '功能管理', diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index f753b868..af5971c1 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -767,7 +767,7 @@ export default { // 从暂停状态恢复播放 this.currentAudio.play().catch((error) => { console.error("恢复播放失败:", error); - this.$message.warning("无法恢复播放音频"); + this.$message.warning(this.$t('roleConfig.cannotResumeAudio')); }); this.isPaused = false; } else { @@ -788,7 +788,7 @@ export default { const targetVoiceId = voiceId || this.form.ttsVoiceId; if (!targetVoiceId) { - this.$message.warning("请先选择一个音色"); + this.$message.warning(this.$t('roleConfig.selectVoiceFirst')); return; } @@ -874,7 +874,7 @@ export default { // 设置超时,防止加载过长时间 const timeoutId = setTimeout(() => { if (this.currentAudio && this.playingVoice) { - this.$message.warning("音频加载时间较长,请稍后重试"); + this.$message.warning(this.$t('roleConfig.audioLoadTimeout')); this.playingVoice = false; } }, 10000); // 10秒超时 @@ -883,7 +883,7 @@ export default { this.currentAudio.onerror = () => { clearTimeout(timeoutId); console.error("克隆音频播放错误"); - this.$message.warning("克隆音频播放失败"); + this.$message.warning(this.$t('roleConfig.cloneAudioPlayFailed')); this.playingVoice = false; }; @@ -905,12 +905,12 @@ export default { this.currentAudio.play().catch((error) => { clearTimeout(timeoutId); console.error("播放克隆音频失败:", error); - this.$message.warning("无法播放克隆音频"); + this.$message.warning(this.$t('roleConfig.cannotPlayCloneAudio')); this.playingVoice = false; }); } else { clearTimeout(timeoutId); - this.$message.warning("获取克隆音频失败"); + this.$message.warning(this.$t('roleConfig.getCloneAudioFailed')); this.playingVoice = false; } }); @@ -950,7 +950,7 @@ export default { if (!audioUrl) { // 如果没有音频URL,显示友好的提示 - this.$message.warning("该音色暂无可预览的音频"); + this.$message.warning(this.$t('roleConfig.noPreviewAudio')); return; } @@ -969,7 +969,7 @@ export default { // 设置超时,防止加载过长时间 const timeoutId = setTimeout(() => { if (this.currentAudio && this.playingVoice) { - this.$message.warning("音频加载时间较长,请稍后重试"); + this.$message.warning(this.$t('roleConfig.audioLoadTimeout')); this.playingVoice = false; } }, 10000); // 10秒超时 @@ -978,7 +978,7 @@ export default { this.currentAudio.onerror = () => { clearTimeout(timeoutId); console.error("音频播放错误"); - this.$message.warning("音频播放失败"); + this.$message.warning(this.$t('roleConfig.audioPlayFailed')); this.playingVoice = false; }; @@ -996,13 +996,13 @@ export default { this.currentAudio.play().catch((error) => { clearTimeout(timeoutId); console.error("播放失败:", error); - this.$message.warning("无法播放音频"); + this.$message.warning(this.$t('roleConfig.cannotPlayAudio')); this.playingVoice = false; }); } } catch (error) { console.error("播放音频过程出错:", error); - this.$message.error("播放音频过程出错"); + this.$message.error(this.$t('roleConfig.audioPlayError')); this.playingVoice = false; } }, From 24519a2732affc672da84e10ccd3ea11700e7546 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Wed, 7 Jan 2026 17:38:38 +0800 Subject: [PATCH 05/11] =?UTF-8?q?TTS=E6=96=87=E6=9C=AC=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/tts/alibl_stream.py | 27 ++++++++++--------- .../core/providers/tts/aliyun_stream.py | 25 ++++++++--------- .../providers/tts/huoshan_double_stream.py | 9 ++++--- .../core/providers/tts/xunfei_stream.py | 8 +++--- main/xiaozhi-server/core/utils/textUtils.py | 2 +- main/xiaozhi-server/core/utils/tts.py | 8 +++++- 6 files changed, 44 insertions(+), 35 deletions(-) diff --git a/main/xiaozhi-server/core/providers/tts/alibl_stream.py b/main/xiaozhi-server/core/providers/tts/alibl_stream.py index 3130cb57..42658008 100644 --- a/main/xiaozhi-server/core/providers/tts/alibl_stream.py +++ b/main/xiaozhi-server/core/providers/tts/alibl_stream.py @@ -185,20 +185,21 @@ class TTSProvider(TTSProviderBase): # 过滤Markdown filtered_text = MarkdownCleaner.clean_markdown(text) - # 发送continue-task消息 - continue_task_message = { - "header": { - "action": "continue-task", - "task_id": self.conn.sentence_id, - "streaming": "duplex", - }, - "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}") + if filtered_text: + # 发送continue-task消息 + continue_task_message = { + "header": { + "action": "continue-task", + "task_id": self.conn.sentence_id, + "streaming": "duplex", + }, + "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}") + return except Exception as e: logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") if self.ws: diff --git a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py index bcbf2b40..52284466 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py @@ -288,18 +288,19 @@ class TTSProvider(TTSProviderBase): logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本") return filtered_text = MarkdownCleaner.clean_markdown(text) - run_request = { - "header": { - "message_id": uuid.uuid4().hex, - "task_id": self.task_id, - "namespace": "FlowingSpeechSynthesizer", - "name": "RunSynthesis", - "appkey": self.appkey, - }, - "payload": {"text": filtered_text}, - } - await self.ws.send(json.dumps(run_request)) - self.last_active_time = time.time() + if filtered_text: + run_request = { + "header": { + "message_id": uuid.uuid4().hex, + "task_id": self.task_id, + "namespace": "FlowingSpeechSynthesizer", + "name": "RunSynthesis", + "appkey": self.appkey, + }, + "payload": {"text": filtered_text}, + } + await self.ws.send(json.dumps(run_request)) + self.last_active_time = time.time() return except Exception as e: diff --git a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py index 38be1560..d7829e35 100644 --- a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py +++ b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py @@ -4,15 +4,15 @@ import json import queue import asyncio import traceback -from typing import Callable, Any import websockets + +from typing import Callable, Any from core.utils.tts import MarkdownCleaner from config.logger import setup_logging from core.utils import opus_encoder_utils from core.utils.util import check_model_key from core.providers.tts.base import TTSProviderBase from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType -from asyncio import Task TAG = __name__ @@ -340,8 +340,9 @@ class TTSProvider(TTSProviderBase): # 过滤Markdown filtered_text = MarkdownCleaner.clean_markdown(text) - # 发送文本 - await self.send_text(self.voice, filtered_text, self.conn.sentence_id) + if filtered_text: + # 发送文本 + await self.send_text(self.voice, filtered_text, self.conn.sentence_id) return except Exception as e: logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") diff --git a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py index d06c9330..dc8d833f 100644 --- a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py +++ b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py @@ -237,10 +237,10 @@ class TTSProvider(TTSProviderBase): return filtered_text = MarkdownCleaner.clean_markdown(text) - - # 发送文本合成请求 - run_request = self._build_base_request(status=1,text=filtered_text) - await self.ws.send(json.dumps(run_request)) + if filtered_text: + # 发送文本合成请求 + run_request = self._build_base_request(status=1,text=filtered_text) + await self.ws.send(json.dumps(run_request)) return except Exception as e: diff --git a/main/xiaozhi-server/core/utils/textUtils.py b/main/xiaozhi-server/core/utils/textUtils.py index 47677b99..2f6afa8c 100644 --- a/main/xiaozhi-server/core/utils/textUtils.py +++ b/main/xiaozhi-server/core/utils/textUtils.py @@ -2,7 +2,7 @@ import json TAG = __name__ EMOJI_MAP = { - "😂": "laughing", + "😂": "funny", "😭": "crying", "😠": "angry", "😔": "sad", diff --git a/main/xiaozhi-server/core/utils/tts.py b/main/xiaozhi-server/core/utils/tts.py index 8b68d195..6727f6de 100644 --- a/main/xiaozhi-server/core/utils/tts.py +++ b/main/xiaozhi-server/core/utils/tts.py @@ -1,9 +1,11 @@ import os import re import sys -from config.logger import setup_logging import importlib +from config.logger import setup_logging +from core.utils.textUtils import check_emoji + logger = setup_logging() punctuation_set = { @@ -135,4 +137,8 @@ class MarkdownCleaner: for regex, replacement in MarkdownCleaner.REGEXES: text = regex.sub(replacement, text) + + # 去除emoji表情 + text = check_emoji(text) + return text.strip() \ No newline at end of file From 40a6b7434ba644d040eb72d2cab924e66ae2dfe9 Mon Sep 17 00:00:00 2001 From: LJH-rgsze <3241304429@qq.com> Date: Wed, 7 Jan 2026 18:08:43 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/views/roleConfig.vue | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index af5971c1..2da34fe3 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -746,17 +746,11 @@ export default { }, // 检查是否有音频预览 hasAudioPreview(item) { - // 使用新增的isClone字段来判断是否为克隆音色 - const isCloneAudio = item.isClone === true; - - // 普通音色的音频检查:同时检查voiceDemo和voice_demo两种命名格式 - const audioField = item.voice_demo || item.voiceDemo; - const hasManualAudio = audioField && - typeof audioField === "string" && - audioField.trim() !== "" && - audioField.toLowerCase().startsWith("http"); - - return isCloneAudio || hasManualAudio; + // 极度简化逻辑: + // 1. 如果hasCloneAudio字段存在且不为false/0,就显示播放按钮 + // 2. 否则检查是否有有效的音频URL + return (item.hasCloneAudio && item.hasCloneAudio !== 'false' && item.hasCloneAudio !== '0') || + !!((item.voice_demo || item.voiceDemo || item.demoUrl || item.audioUrl || item.sample_voice || item.referenceAudio || item.cloneAudioUrl)?.trim()); }, // 播放/暂停音频切换 From c1959e079541abf03a0a7e7a3f78fa5e686f2eb2 Mon Sep 17 00:00:00 2001 From: LJH-rgsze <3241304429@qq.com> Date: Thu, 8 Jan 2026 10:10:39 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/views/roleConfig.vue | 41 ++++++++++------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index 2da34fe3..2beff38a 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -599,17 +599,12 @@ export default { this.voiceOptions = data.data.map((voice) => ({ value: voice.id, label: voice.name, - // 复制音频相关字段,确保hasAudioPreview能检测到 + // 只保留后端实际返回的音频相关字段 voiceDemo: voice.voiceDemo, - demoUrl: voice.demoUrl, - audioUrl: voice.audioUrl, voice_demo: voice.voice_demo, - sample_voice: voice.sample_voice, - referenceAudio: voice.referenceAudio, - // 新增:添加克隆音频相关字段 - cloneAudioUrl: voice.cloneAudioUrl, - hasCloneAudio: voice.hasCloneAudio || false, - // 保存训练状态字段,用于判断是否为克隆音频 + // 使用后端实际返回的 isClone 字段 + isClone: Boolean(voice.isClone), + // 保存训练状态字段 train_status: voice.trainStatus, })); // 保存完整的音色信息,添加调试信息 @@ -746,11 +741,15 @@ export default { }, // 检查是否有音频预览 hasAudioPreview(item) { - // 极度简化逻辑: - // 1. 如果hasCloneAudio字段存在且不为false/0,就显示播放按钮 - // 2. 否则检查是否有有效的音频URL - return (item.hasCloneAudio && item.hasCloneAudio !== 'false' && item.hasCloneAudio !== '0') || - !!((item.voice_demo || item.voiceDemo || item.demoUrl || item.audioUrl || item.sample_voice || item.referenceAudio || item.cloneAudioUrl)?.trim()); + // 检查是否为克隆音频 + // 使用后端实际返回的 isClone 字段 + const isCloneAudio = Boolean(item.isClone); + + // 检查是否有有效的音频URL,只使用后端实际返回的字段 + const hasValidAudioUrl = !!((item.voice_demo || item.voiceDemo)?.trim()); + + // 克隆音频始终显示播放按钮,普通音频需要有有效URL才显示 + return isCloneAudio || hasValidAudioUrl; }, // 播放/暂停音频切换 @@ -809,10 +808,8 @@ export default { let isCloneAudio = false; if (voiceDetail) { - // 首先检查是否是克隆音频(通过ID格式判断,非TTS开头的ID) - isCloneAudio = - voiceDetail.hasCloneAudio || - (voiceDetail.id && !voiceDetail.id.startsWith("TTS")); + // 使用后端实际返回的 isClone 字段判断是否为克隆音频 + isCloneAudio = Boolean(voiceDetail.isClone); console.log( "克隆音频判断结果:", isCloneAudio, @@ -912,14 +909,10 @@ export default { // 返回,避免继续执行下面的普通音频播放逻辑 return; } else { - // 对于普通音频,尝试各种可能的URL字段 + // 对于普通音频,只使用后端实际返回的字段 audioUrl = voiceDetail.voiceDemo || - voiceDetail.demoUrl || - voiceDetail.audioUrl || - voiceDetail.voice_demo || - voiceDetail.sample_voice || - voiceDetail.cloneAudioUrl; // 克隆音频URL + voiceDetail.voice_demo; } // 如果没有找到,尝试检查是否有URL格式的字段 From c1bf92d243e786993614a2dccdf14dd4034f252f Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Thu, 8 Jan 2026 11:39:04 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E9=9F=B3=E9=A2=91=E6=97=B6=E6=89=93=E6=96=AD?= =?UTF-8?q?=E5=90=8E=E4=BD=BF=E7=94=A8=E5=94=A4=E9=86=92=E8=AF=8D=E6=97=B6?= =?UTF-8?q?=E6=B5=81=E6=8E=A7=E5=99=A8=E6=9C=AA=E9=87=8D=E7=BD=AE=E9=80=A0?= =?UTF-8?q?=E6=88=90=E7=9A=84=E6=AD=BB=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/handle/sendAudioHandle.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py index 8198dfb0..e60ce412 100644 --- a/main/xiaozhi-server/core/handle/sendAudioHandle.py +++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py @@ -133,12 +133,28 @@ def _get_or_create_rate_controller(conn, frame_duration, is_single_packet): Returns: (rate_controller, flow_control) """ - # 判断是否需要重置:单包模式且 sentence_id 变化,或者控制器不存在 - need_reset = ( - is_single_packet - and getattr(conn, "audio_flow_control", {}).get("sentence_id") - != conn.sentence_id - ) or not hasattr(conn, "audio_rate_controller") + # 检查是否需要重置控制器 + need_reset = False + + if 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 ( + is_single_packet + and getattr(conn, "audio_flow_control", {}).get("sentence_id") + != conn.sentence_id + ): + need_reset = True if need_reset: # 创建或获取 rate_controller From 3a61ec84b30eed2c49dfb1f74f531736feaea044 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 8 Jan 2026 12:39:07 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E7=A7=BB=E9=99=A4console=E6=89=93?= =?UTF-8?q?=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/views/roleConfig.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index 2beff38a..580b6989 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -608,7 +608,6 @@ export default { train_status: voice.trainStatus, })); // 保存完整的音色信息,添加调试信息 - console.log("获取到的音色数据:", data.data); this.voiceDetails = data.data.reduce((acc, voice) => { acc[voice.id] = voice; return acc; From 54cac0d2fd0bae402bf2bbdbdb7ad0c62f8fa42e Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Thu, 8 Jan 2026 16:17:48 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BD=93?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E8=AE=BE=E5=A4=87=E4=B8=8A=E4=B8=AA=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=AD=A3=E5=B8=B8=E7=BB=93=E6=9D=9F=E5=90=8E=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=94=A4=E9=86=92=E8=AF=8D=E5=94=A4=E9=86=92?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E6=92=AD=E6=94=BE=E4=B8=8D=E5=AE=8C=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/handle/helloHandle.py | 4 ++++ main/xiaozhi-server/core/handle/sendAudioHandle.py | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/main/xiaozhi-server/core/handle/helloHandle.py b/main/xiaozhi-server/core/handle/helloHandle.py index a4220d5a..129b0aa7 100644 --- a/main/xiaozhi-server/core/handle/helloHandle.py +++ b/main/xiaozhi-server/core/handle/helloHandle.py @@ -1,5 +1,6 @@ import time import json +import uuid import random import asyncio from core.utils.dialogue import Message @@ -105,6 +106,9 @@ async def checkWakeupWords(conn, text): # 播放唤醒词回复 conn.client_abort = False + # 将唤醒词回复视为新会话,生成新的 sentence_id,确保流控器重置 + conn.sentence_id = str(uuid.uuid4().hex) + conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response.get('text')}") await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response.get("text")) await sendAudioMessage(conn, SentenceType.LAST, [], None) diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py index e60ce412..323056d1 100644 --- a/main/xiaozhi-server/core/handle/sendAudioHandle.py +++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py @@ -148,10 +148,9 @@ def _get_or_create_rate_controller(conn, frame_duration, is_single_packet): or rate_controller.pending_send_task.done() ): need_reset = True - # 流式单包模式下, sentence_id 变化, 需要重置 + # 当sentence_id 变化,需要重置 elif ( - is_single_packet - and getattr(conn, "audio_flow_control", {}).get("sentence_id") + getattr(conn, "audio_flow_control", {}).get("sentence_id") != conn.sentence_id ): need_reset = True From ad93f431453358f0e7944b286c45e79719ffd7be Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 9 Jan 2026 10:49:24 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=EF=BC=8C=E8=A1=A5=E5=85=85=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=AD=BE=E5=90=8D=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/memory/powermem/powermem.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index c1891932..1689b423 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -134,13 +134,14 @@ class MemoryProvider(MemoryProviderBase): logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}") self.use_powermem = False - async def save_memory(self, msgs): + async def save_memory(self, msgs, session_id=None): """ Save conversation messages to PowerMem. - + Args: msgs: List of message objects with 'role' and 'content' attributes - + session_id: Session identifier (optional, for compatibility) + Returns: Result from PowerMem API or None if failed """ @@ -293,7 +294,3 @@ class MemoryProvider(MemoryProviderBase): logger.bind(tag=TAG).error(f"Error getting user profile: {str(e)}") logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}") return "" - - -# Register the memory provider instance -powermem = MemoryProvider({})