From 788fb1fc17fe29bec1381a9abab6883cebf537f8 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sat, 12 Apr 2025 11:57:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=20TTS=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=9C=8D=E5=8A=A1=E5=B9=B6=E6=9B=B4=E6=96=B0=E4=BA=86?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron-builder.yml | 2 + package.json | 1 + packages/shared/IpcChannel.ts | 3 + src/main/services/MsTTSIpcHandler.ts | 32 +- src/main/services/MsTTSService.ts | 84 +++- src/renderer/src/components/ASRButton.tsx | 4 +- src/renderer/src/components/TTSButton.tsx | 53 ++- .../src/pages/home/Inputbar/Inputbar.tsx | 67 ++- .../src/pages/home/Messages/Message.tsx | 21 +- .../src/pages/home/Messages/TTSStopButton.tsx | 33 +- src/renderer/src/services/ASRService.ts | 36 +- src/renderer/src/services/VoiceCallService.ts | 36 +- .../src/services/tts/AudioStreamProcessor.ts | 90 ++++ .../src/services/tts/EdgeTTSService.ts | 16 +- src/renderer/src/services/tts/TTSService.ts | 93 ++-- .../src/services/tts/TTSServiceInterface.ts | 11 + temp.txt | 8 + yarn.lock | 415 +++++++++++++++++- 18 files changed, 866 insertions(+), 139 deletions(-) create mode 100644 src/renderer/src/services/tts/AudioStreamProcessor.ts create mode 100644 temp.txt diff --git a/electron-builder.yml b/electron-builder.yml index df6576c58a..29c8c97e47 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,6 +3,8 @@ productName: Cherry Studio directories: buildResources: build files: + - out/**/* + - package.json - '!{.vscode,.yarn,.github}' - '!electron.vite.config.{js,ts,mjs,cjs}' - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' diff --git a/package.json b/package.json index 60efc83444..6d591845eb 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "color": "^5.0.0", "diff": "^7.0.0", "docx": "^9.0.2", + "edge-tts-node": "^1.5.7", "electron-log": "^5.1.5", "electron-store": "^8.2.0", "electron-updater": "^6.3.9", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 5975737b0c..92f14549f5 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -25,6 +25,9 @@ export enum IpcChannel { // MsTTS MsTTS_GetVoices = 'mstts:get-voices', MsTTS_Synthesize = 'mstts:synthesize', + MsTTS_SynthesizeStream = 'mstts:synthesize-stream', + MsTTS_StreamData = 'mstts:stream-data', + MsTTS_StreamEnd = 'mstts:stream-end', // Open Open_Path = 'open:path', diff --git a/src/main/services/MsTTSIpcHandler.ts b/src/main/services/MsTTSIpcHandler.ts index 083d4b3554..66292b09bd 100644 --- a/src/main/services/MsTTSIpcHandler.ts +++ b/src/main/services/MsTTSIpcHandler.ts @@ -1,5 +1,5 @@ import { IpcChannel } from '@shared/IpcChannel' -import { ipcMain } from 'electron' +import { BrowserWindow, ipcMain } from 'electron' import * as MsTTSService from './MsTTSService' @@ -14,4 +14,34 @@ export function registerMsTTSIpcHandlers(): void { ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) => MsTTSService.synthesize(text, voice, outputFormat) ) + + // 流式合成语音 + ipcMain.handle(IpcChannel.MsTTS_SynthesizeStream, async (event, requestId: string, text: string, voice: string, outputFormat: string) => { + const window = BrowserWindow.fromWebContents(event.sender) + if (!window) return + + try { + await MsTTSService.synthesizeStream( + text, + voice, + outputFormat, + (chunk: Uint8Array) => { + // 发送音频数据块 + if (!window.isDestroyed()) { + window.webContents.send(IpcChannel.MsTTS_StreamData, requestId, chunk) + } + }, + () => { + // 发送流结束信号 + if (!window.isDestroyed()) { + window.webContents.send(IpcChannel.MsTTS_StreamEnd, requestId) + } + } + ) + return { success: true } + } catch (error) { + console.error('流式TTS合成失败:', error) + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + }) } diff --git a/src/main/services/MsTTSService.ts b/src/main/services/MsTTSService.ts index 30ced63fc8..2a056b80a7 100644 --- a/src/main/services/MsTTSService.ts +++ b/src/main/services/MsTTSService.ts @@ -3,7 +3,8 @@ import path from 'node:path' import { app } from 'electron' import log from 'electron-log' -import { EdgeTTS } from 'node-edge-tts' // listVoices is no longer needed here +import { EdgeTTS } from 'node-edge-tts' // 旧版TTS库 +import { MsEdgeTTS, OUTPUT_FORMAT } from 'edge-tts-node' // 新版支持流式的TTS库 // --- START OF HARDCODED VOICE LIST --- // WARNING: This list is static and may become outdated. @@ -437,6 +438,77 @@ class MsTTSService { return MsTTSService.instance } + /** + * 流式合成语音 + * @param text 要合成的文本 + * @param voice 语音的 ShortName (例如 'zh-CN-XiaoxiaoNeural') + * @param outputFormat 输出格式 (例如 'audio-24khz-48kbitrate-mono-mp3') + * @param onData 数据块回调 + * @param onEnd 结束回调 + */ + public async synthesizeStream( + text: string, + voice: string, + outputFormat: string, + onData: (chunk: Uint8Array) => void, + onEnd: () => void + ): Promise { + try { + // 记录详细的请求信息 + log.info(`流式微软在线TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`) + + // 验证输入参数 + if (!text || text.trim() === '') { + throw new Error('要合成的文本不能为空') + } + + if (!voice || voice.trim() === '') { + throw new Error('语音名称不能为空') + } + + // 创建一个新的MsEdgeTTS实例 + const tts = new MsEdgeTTS({ + enableLogger: false // 禁用内部日志 + }) + + // 设置元数据 + let msOutputFormat: OUTPUT_FORMAT + if (outputFormat.includes('mp3')) { + msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3 + } else if (outputFormat.includes('webm')) { + msOutputFormat = OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS + } else { + msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3 + } + + await tts.setMetadata(voice, msOutputFormat) + + // 创建流 + const audioStream = tts.toStream(text) + + // 监听数据事件 + audioStream.on('data', (data: Buffer) => { + onData(data) + }) + + // 监听结束事件 + audioStream.on('end', () => { + log.info(`流式微软在线TTS合成成功`) + onEnd() + }) + + // 监听错误事件 + audioStream.on('error', (error: Error) => { + log.error(`流式微软在线TTS语音合成失败:`, error) + throw error + }) + } catch (error: any) { + // 记录详细的错误信息 + log.error(`流式微软在线TTS语音合成失败 (语音=${voice}):`, error) + throw error + } + } + /** * 获取可用的语音列表 (返回硬编码列表) * @returns 语音列表 @@ -556,6 +628,16 @@ export const synthesize = async (text: string, voice: string, outputFormat: stri return await MsTTSService.getInstance().synthesize(text, voice, outputFormat) } +export const synthesizeStream = async ( + text: string, + voice: string, + outputFormat: string, + onData: (chunk: Uint8Array) => void, + onEnd: () => void +) => { + return await MsTTSService.getInstance().synthesizeStream(text, voice, outputFormat, onData, onEnd) +} + export const cleanupTtsTempFiles = async () => { await MsTTSService.getInstance().cleanupTempDir() } diff --git a/src/renderer/src/components/ASRButton.tsx b/src/renderer/src/components/ASRButton.tsx index 89ad0347d9..3987e0ae26 100644 --- a/src/renderer/src/components/ASRButton.tsx +++ b/src/renderer/src/components/ASRButton.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import styled from 'styled-components' interface Props { - onTranscribed: (text: string) => void + onTranscribed: (text: string, isFinal?: boolean) => void disabled?: boolean style?: React.CSSProperties } @@ -33,7 +33,7 @@ const ASRButton: FC = ({ onTranscribed, disabled = false, style }) => { try { // 添加事件监听器,监听服务器发送的stopped消息 const originalCallback = ASRService.resultCallback - const stopCallback = (text: string) => { + const stopCallback = (text: string, isFinal?: boolean) => { // 如果是空字符串,只重置状态,不调用原始回调 if (text === '') { setIsProcessing(false) diff --git a/src/renderer/src/components/TTSButton.tsx b/src/renderer/src/components/TTSButton.tsx index aa6592f372..9e70eb4cd1 100644 --- a/src/renderer/src/components/TTSButton.tsx +++ b/src/renderer/src/components/TTSButton.tsx @@ -2,7 +2,7 @@ import { SoundOutlined } from '@ant-design/icons' import TTSService from '@renderer/services/TTSService' import { Message } from '@renderer/types' import { Button, Tooltip } from 'antd' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' interface TTSButtonProps { @@ -14,38 +14,45 @@ const TTSButton: React.FC = ({ message, className }) => { const { t } = useTranslation() const [isSpeaking, setIsSpeaking] = useState(false) + // 添加TTS状态变化事件监听器 + useEffect(() => { + const handleTTSStateChange = (event: CustomEvent) => { + const { isPlaying } = event.detail + console.log('TTS按钮检测到TTS状态变化:', isPlaying) + setIsSpeaking(isPlaying) + } + + // 添加事件监听器 + window.addEventListener('tts-state-change', handleTTSStateChange as EventListener) + + // 组件卸载时移除事件监听器 + return () => { + window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener) + } + }, []) + + // 初始化时检查TTS状态 + useEffect(() => { + // 检查当前是否正在播放 + const isCurrentlyPlaying = TTSService.isCurrentlyPlaying() + if (isCurrentlyPlaying !== isSpeaking) { + setIsSpeaking(isCurrentlyPlaying) + } + }, []) + const handleTTS = useCallback(async () => { if (isSpeaking) { TTSService.stop() - setIsSpeaking(false) - return + return // 不需要手动设置状态,事件监听器会处理 } - setIsSpeaking(true) try { console.log('点击TTS按钮,开始播放消息') await TTSService.speakFromMessage(message) - - // 监听播放结束 - const checkPlayingStatus = () => { - if (!TTSService.isCurrentlyPlaying()) { - setIsSpeaking(false) - clearInterval(checkInterval) - } - } - - const checkInterval = setInterval(checkPlayingStatus, 500) - - // 安全机制,确保即使出错也会重置状态 - setTimeout(() => { - if (isSpeaking) { - TTSService.stop() - setIsSpeaking(false) - clearInterval(checkInterval) - } - }, 30000) // 30秒后检查 + // 不需要手动设置状态,事件监听器会处理 } catch (error) { console.error('TTS error:', error) + // 出错时才需要手动重置状态 setIsSpeaking(false) } }, [isSpeaking, message]) diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index fd870587ea..97845a9a7a 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -77,6 +77,7 @@ let _files: FileType[] = [] const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) => { const [text, setText] = useState(_text) + const [asrCurrentText, setAsrCurrentText] = useState('') const [inputFocus, setInputFocus] = useState(false) const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id) const { @@ -787,18 +788,40 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } } - // 如果是语音通话消息,创建一个新的助手对象,并设置模型 + // 如果是语音通话消息,创建一个新的助手对象,并设置模型和提示词 let assistantToUse = assistant - if ((data.isVoiceCall || data.useVoiceCallModel) && userMessage.model) { + if (data.isVoiceCall || data.useVoiceCallModel) { // 创建一个新的助手对象,以避免修改原始助手 assistantToUse = { ...assistant } - // 设置助手的模型为语音通话专用模型 - assistantToUse.model = userMessage.model - console.log( - '为语音通话消息创建了新的助手对象,并设置了模型:', - userMessage.model.name || userMessage.model.id - ) + // 如果有语音通话专用模型,设置助手的模型 + if (userMessage.model) { + assistantToUse.model = userMessage.model + console.log( + '为语音通话消息创建了新的助手对象,并设置了模型:', + userMessage.model.name || userMessage.model.id + ) + } + + // 添加语音通话专属提示词 + const voiceCallPrompt = `当前是语音通话模式。请注意: +1. 简洁直接地回答问题,避免冗长的引导和总结。 +2. 避免使用复杂的格式化内容,如表格、代码块、Markdown等。 +3. 使用自然、口语化的表达方式,就像与人对话一样。 +4. 如果需要列出要点,使用简单的数字或文字标记,而不是复杂的格式。 +5. 回答应该简短有力,便于用户通过语音理解。 +6. 避免使用特殊符号、表情符号、标点符号等,因为这些在语音播放时会影响理解。 +7. 使用完整的句子而非简单的关键词列表。 +8. 尽量使用常见词汇,避免生僻或专业术语,除非用户特别询问。` + + // 如果助手已经有提示词,则在其后添加语音通话专属提示词 + if (assistantToUse.prompt) { + assistantToUse.prompt += '\n\n' + voiceCallPrompt + } else { + assistantToUse.prompt = voiceCallPrompt + } + + console.log('为语音通话消息添加了专属提示词') } // 分发发送消息的action @@ -806,6 +829,8 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = // 清空输入框 setText('') + // 重置语音识别状态 + setAsrCurrentText('') console.log('已触发发送消息事件') }, 300) @@ -1121,18 +1146,28 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = { + onTranscribed={(transcribedText, isFinal) => { // 如果是空字符串,不做任何处理 if (!transcribedText) return - // 将识别的文本添加到当前输入框 - setText((prevText) => { - // 如果当前有文本,添加空格后再添加识别的文本 - if (prevText.trim()) { + if (isFinal) { + // 最终结果,添加到输入框中 + setText((prevText) => { + // 如果当前输入框为空,直接设置为识别的文本 + if (!prevText.trim()) { + return transcribedText + } + + // 否则,添加识别的文本到输入框中,用空格分隔 return prevText + ' ' + transcribedText - } - return transcribedText - }) + }) + + // 清除当前识别的文本 + setAsrCurrentText('') + } else { + // 中间结果,保存到状态变量中,但不更新输入框 + setAsrCurrentText(transcribedText) + } }} /> diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 565e4b3723..36ddc2c4da 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -153,15 +153,20 @@ const MessageItem: FC = ({ // 自动播放TTS的逻辑 useEffect(() => { - // 如果是最后一条助手消息,且消息状态为成功,且不是正在生成中,且TTS已启用,且语音通话窗口已打开 + // 如果是最后一条助手消息,且消息状态为成功,且不是正在生成中,且TTS已启用 + // 注意:只有在语音通话窗口打开时才自动播放TTS if ( isLastMessage && isAssistantMessage && message.status === 'success' && !generating && - ttsEnabled && - isVoiceCallActive + ttsEnabled ) { + // 如果语音通话窗口没有打开,则不自动播放TTS + if (!isVoiceCallActive) { + console.log('不自动播放TTS,因为语音通话窗口没有打开:', isVoiceCallActive) + return + } // 检查是否需要跳过自动TTS if (skipNextAutoTTS) { console.log( @@ -205,16 +210,6 @@ const MessageItem: FC = ({ } else if (message.id === lastPlayedMessageId) { console.log('不自动播放TTS,因为该消息已经播放过:', message.id) } - } else if ( - isLastMessage && - isAssistantMessage && - message.status === 'success' && - !generating && - ttsEnabled && - !isVoiceCallActive - ) { - // 如果语音通话窗口没有打开,则不自动播放TTS - console.log('不自动播放TTS,因为语音通话窗口没有打开') } }, [ isLastMessage, diff --git a/src/renderer/src/pages/home/Messages/TTSStopButton.tsx b/src/renderer/src/pages/home/Messages/TTSStopButton.tsx index f5065b2821..96cfc50211 100644 --- a/src/renderer/src/pages/home/Messages/TTSStopButton.tsx +++ b/src/renderer/src/pages/home/Messages/TTSStopButton.tsx @@ -9,14 +9,25 @@ const TTSStopButton: React.FC = () => { const { t } = useTranslation() const [isVisible, setIsVisible] = useState(false) - // 检查是否正在播放TTS + // 添加TTS状态变化事件监听器 useEffect(() => { - const checkPlayingStatus = setInterval(() => { - const isPlaying = TTSService.isCurrentlyPlaying() + const handleTTSStateChange = (event: CustomEvent) => { + const { isPlaying } = event.detail + console.log('全局TTS停止按钮检测到TTS状态变化:', isPlaying) setIsVisible(isPlaying) - }, 500) + } - return () => clearInterval(checkPlayingStatus) + // 添加事件监听器 + window.addEventListener('tts-state-change', handleTTSStateChange as EventListener) + + // 初始检查当前状态 + const isCurrentlyPlaying = TTSService.isCurrentlyPlaying() + setIsVisible(isCurrentlyPlaying) + + // 组件卸载时移除事件监听器 + return () => { + window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener) + } }, []) // 停止TTS播放 @@ -26,17 +37,7 @@ const TTSStopButton: React.FC = () => { // 强制停止所有TTS播放 TTSService.stop() - // 等待一下,确保播放已经完全停止 - await new Promise((resolve) => setTimeout(resolve, 100)) - - // 再次检查并停止,确保强制停止 - if (TTSService.isCurrentlyPlaying()) { - console.log('第一次停止未成功,再次尝试') - TTSService.stop() - } - - // 立即隐藏按钮 - setIsVisible(false) + // 不需要手动设置状态,事件监听器会处理 // 显示停止消息 window.message.success({ content: t('chat.tts.stopped', { defaultValue: '已停止语音播放' }), key: 'tts-stopped' }) diff --git a/src/renderer/src/services/ASRService.ts b/src/renderer/src/services/ASRService.ts index 3288da6b23..19b699bafd 100644 --- a/src/renderer/src/services/ASRService.ts +++ b/src/renderer/src/services/ASRService.ts @@ -163,15 +163,14 @@ class ASRService { if (data.data.isFinal) { console.log('[ASRService] 收到最终结果,调用回调函数,文本:', data.data.text) - // 保存当前回调函数并立即清除,防止重复处理 - const tempCallback = this.resultCallback - this.resultCallback = null + // 不再清除回调函数,允许继续处理后续语音 + // const tempCallback = this.resultCallback + // this.resultCallback = null - // 调用回调函数 - tempCallback(data.data.text, true) + // 直接调用回调函数 + this.resultCallback(data.data.text, true) window.message.success({ content: i18n.t('settings.asr.success'), key: 'asr-processing' }) - } else if (this.isRecording) { - // 只在录音中才处理中间结果 + } else if (this.isRecording) { // 只在录音中才处理中间结果 // 非最终结果,也调用回调,但标记为非最终 console.log('[ASRService] 收到中间结果,调用回调函数,文本:', data.data.text) this.resultCallback(data.data.text, false) @@ -237,6 +236,14 @@ class ASRService { return } + // 先设置回调函数,确保在任何情况下都能正确设置 + if (onTranscribed && typeof onTranscribed === 'function') { + console.log('[ASRService] 设置结果回调函数') + this.resultCallback = onTranscribed + } else { + console.warn('[ASRService] 未提供有效的回调函数') + } + // 如果是使用本地服务器 if (asrServiceType === 'local') { // 连接WebSocket服务器 @@ -292,11 +299,6 @@ class ASRService { } } - // 保存回调函数(如果提供了) - if (onTranscribed && typeof onTranscribed === 'function') { - this.resultCallback = onTranscribed - } - // 发送开始命令 if (this.ws && this.wsConnected) { this.ws.send(JSON.stringify({ type: 'start' })) @@ -376,11 +378,11 @@ class ASRService { }, 100) } - // 添加额外的安全措施,确保在停止后也清除回调 - setTimeout(() => { - // 在停止后的一段时间内清除回调,防止后续结果被处理 - this.resultCallback = null - }, 3000) // 3秒后清除回调 + // 不再清除回调函数,允许连续说多句话 + // setTimeout(() => { + // // 在停止后的一段时间内清除回调,防止后续结果被处理 + // this.resultCallback = null + // }, 3000) // 3秒后清除回调 } else { throw new Error('WebSocket连接未就绪') } diff --git a/src/renderer/src/services/VoiceCallService.ts b/src/renderer/src/services/VoiceCallService.ts index de66a7edcf..ecf6836715 100644 --- a/src/renderer/src/services/VoiceCallService.ts +++ b/src/renderer/src/services/VoiceCallService.ts @@ -257,8 +257,8 @@ class VoiceCallServiceClass { } else { // 如果是临时结果,更新当前的识别结果 this._currentTranscript = text - // 显示累积结果 + 当前临时结果 - this.callbacks?.onTranscript(this._accumulatedTranscript + ' ' + text) + // 只显示当前临时结果,不与累积结果拼接 + this.callbacks?.onTranscript(text) } // 在录音过程中只更新transcript,不触发handleUserSpeech @@ -595,12 +595,10 @@ class VoiceCallServiceClass { 8. 尽量使用常见词汇,避免生僻或专业术语,除非用户特别询问。` // 创建系统指令消息 - const systemMessage = getUserMessage({ - assistant, - topic, - type: 'text', + const systemMessage = { + role: 'system', content: voiceCallPrompt - }) + } // 修改用户消息的内容 userMessage.content = text @@ -637,14 +635,24 @@ class VoiceCallServiceClass { if (!this.isMuted && this.isCallActive) { // 手动设置语音状态 this.callbacks?.onSpeakingStateChange(true) + + // 添加TTS状态变化事件监听器 + const handleTTSStateChange = (event: CustomEvent) => { + const { isPlaying } = event.detail + console.log('语音通话中检测到TTS状态变化:', isPlaying) + this.callbacks?.onSpeakingStateChange(isPlaying) + } + + // 添加事件监听器 + window.addEventListener('tts-state-change', handleTTSStateChange as EventListener) + + // 开始播放 this.ttsService.speak(fullResponse) - // 确保语音结束后状态正确 + // 设置超时安全机制,确保事件监听器被移除 setTimeout(() => { - if (this.ttsService && !this.ttsService.isCurrentlyPlaying()) { - this.callbacks?.onSpeakingStateChange(false) - } - }, 1000) // 1秒后检查TTS状态 + window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener) + }, 30000) // 30秒后移除事件监听器 } // 更新对话历史 @@ -910,9 +918,7 @@ class VoiceCallServiceClass { this.ttsService.stop() console.log('强制停止TTS播放') - // 手动触发TTS状态变化事件,确保 UI 状态更新 - const event = new CustomEvent('tts-state-change', { detail: { isPlaying: false } }) - window.dispatchEvent(event) + // 注意:不需要手动触发事件,因为在TTSService.stop()中已经触发了 } setPaused(paused: boolean) { diff --git a/src/renderer/src/services/tts/AudioStreamProcessor.ts b/src/renderer/src/services/tts/AudioStreamProcessor.ts new file mode 100644 index 0000000000..be368431a7 --- /dev/null +++ b/src/renderer/src/services/tts/AudioStreamProcessor.ts @@ -0,0 +1,90 @@ +/** + * 音频流处理器 + * 用于处理流式TTS的音频数据 + */ +export class AudioStreamProcessor { + private audioContext: AudioContext | null = null + private audioQueue: Uint8Array[] = [] + private isProcessing: boolean = false + + // 回调函数 + public onAudioBuffer: ((buffer: AudioBuffer) => void) | null = null + + /** + * 初始化音频处理器 + */ + public async initialize(): Promise { + // 创建音频上下文 + this.audioContext = new AudioContext() + this.audioQueue = [] + this.isProcessing = false + } + + /** + * 处理音频数据块 + * @param chunk 音频数据块 + */ + public async processAudioChunk(chunk: Uint8Array): Promise { + if (!this.audioContext) { + throw new Error('AudioStreamProcessor not initialized') + } + + // 将数据块添加到队列 + this.audioQueue.push(chunk) + + // 如果没有正在处理,开始处理 + if (!this.isProcessing) { + this.processQueue() + } + } + + /** + * 处理队列中的音频数据 + */ + private async processQueue(): Promise { + if (!this.audioContext || this.audioQueue.length === 0) { + this.isProcessing = false + return + } + + this.isProcessing = true + + // 获取队列中的第一个数据块 + const chunk = this.audioQueue.shift()! + + try { + // 解码音频数据 + // 将SharedArrayBuffer转换为ArrayBuffer + const arrayBuffer = chunk.buffer instanceof SharedArrayBuffer + ? new Uint8Array(chunk.buffer).buffer + : chunk.buffer + const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer) + + // 调用回调函数 + if (this.onAudioBuffer) { + this.onAudioBuffer(audioBuffer) + } + } catch (error) { + console.error('解码音频数据失败:', error) + } + + // 继续处理队列中的下一个数据块 + this.processQueue() + } + + /** + * 完成处理 + */ + public async finish(): Promise { + // 等待队列处理完成 + while (this.audioQueue.length > 0) { + await new Promise(resolve => setTimeout(resolve, 100)) + } + + // 关闭音频上下文 + if (this.audioContext) { + await this.audioContext.close() + this.audioContext = null + } + } +} diff --git a/src/renderer/src/services/tts/EdgeTTSService.ts b/src/renderer/src/services/tts/EdgeTTSService.ts index 72bd2d364d..846df9b46b 100644 --- a/src/renderer/src/services/tts/EdgeTTSService.ts +++ b/src/renderer/src/services/tts/EdgeTTSService.ts @@ -5,6 +5,9 @@ import { TTSServiceInterface } from './TTSServiceInterface' // 全局变量来跟踪当前正在播放的语音 let currentUtterance: SpeechSynthesisUtterance | null = null +// 全局变量来跟踪是否正在播放 +export let isEdgeTTSPlaying = false + /** * Edge TTS服务实现类 */ @@ -50,10 +53,12 @@ export class EdgeTTSService implements TTSServiceInterface { if (currentUtterance) { currentUtterance = null } + isEdgeTTSPlaying = false // 创建语音合成器实例 const utterance = new SpeechSynthesisUtterance(text) currentUtterance = utterance + isEdgeTTSPlaying = true // 获取可用的语音合成声音 const voices = window.speechSynthesis.getVoices() @@ -93,6 +98,7 @@ export class EdgeTTSService implements TTSServiceInterface { utterance.onend = () => { console.log('语音合成已结束') currentUtterance = null + isEdgeTTSPlaying = false // 分发一个自定义事件,通知语音合成已结束 // 这样TTSService可以监听这个事件并重置播放状态 @@ -100,9 +106,14 @@ export class EdgeTTSService implements TTSServiceInterface { document.dispatchEvent(event) } - utterance.onerror = (event) => { - console.error('语音合成错误:', event) + utterance.onerror = (errorEvent) => { + console.error('语音合成错误:', errorEvent) currentUtterance = null + isEdgeTTSPlaying = false + + // 在错误时也触发结束事件,确保状态更新 + const completeEvent = new CustomEvent('edgeTTSComplete', { detail: { text, error: true } }) + document.dispatchEvent(completeEvent) } // 开始语音合成 @@ -147,6 +158,7 @@ export class EdgeTTSService implements TTSServiceInterface { // 停止当前正在播放的语音 window.speechSynthesis.cancel() + isEdgeTTSPlaying = false // 创建语音合成器实例 const utterance = new SpeechSynthesisUtterance(text) diff --git a/src/renderer/src/services/tts/TTSService.ts b/src/renderer/src/services/tts/TTSService.ts index 7dbe2aa0eb..508ead169b 100644 --- a/src/renderer/src/services/tts/TTSService.ts +++ b/src/renderer/src/services/tts/TTSService.ts @@ -14,6 +14,7 @@ export class TTSService { private static instance: TTSService private audioElement: HTMLAudioElement | null = null private isPlaying = false + private playingServiceType: string | null = null // 错误消息节流控制 private lastErrorTime = 0 @@ -42,15 +43,34 @@ export class TTSService { // 监听音频播放结束事件 this.audioElement.addEventListener('ended', () => { - this.isPlaying = false - console.log('TTS播放结束') + // 只有在非EdgeTTS服务时才直接更新状态 + if (this.playingServiceType !== 'edge') { + this.updatePlayingState(false) + console.log('TTS播放结束 (音频元素事件)') + } }) // 监听浏览器TTS直接播放结束的自定义事件 document.addEventListener('edgeTTSComplete', () => { console.log('收到浏览器TTS直接播放结束事件') - this.isPlaying = false + this.updatePlayingState(false) }) + + // 监听全局的speechSynthesis状态 + if ('speechSynthesis' in window) { + // 创建一个定时器,定期检查speechSynthesis的状态 + setInterval(() => { + // 只有在使用EdgeTTS且标记为正在播放时才检查 + if (this.isPlaying && this.playingServiceType === 'edge') { + // 检查是否还在播放 + const isSpeaking = window.speechSynthesis.speaking; + if (!isSpeaking) { + console.log('检测到speechSynthesis不再播放,更新状态') + this.updatePlayingState(false); + } + } + }, 500); // 每500毫秒检查一次 + } } /** @@ -82,6 +102,32 @@ export class TTSService { return this.speak(filteredText) } + /** + * 更新播放状态并触发事件 + * @param isPlaying 是否正在播放 + */ + private updatePlayingState(isPlaying: boolean): void { + // 只有状态变化时才更新和触发事件 + if (this.isPlaying !== isPlaying) { + this.isPlaying = isPlaying; + console.log(`TTS播放状态更新: ${isPlaying ? '开始播放' : '停止播放'}`) + + // 触发自定义事件,通知其他组件TTS状态变化 + const event = new CustomEvent('tts-state-change', { detail: { isPlaying } }) + window.dispatchEvent(event) + + // 如果停止播放,清除服务类型 + if (!isPlaying) { + this.playingServiceType = null; + + // 确保Web Speech API也停止 + if ('speechSynthesis' in window) { + window.speechSynthesis.cancel(); + } + } + } + } + /** * 播放文本 * @param text 要播放的文本 @@ -101,6 +147,8 @@ export class TTSService { // 如果正在播放,先停止 if (this.isPlaying) { this.stop() + // 添加短暂延迟,确保上一个播放完全停止 + await new Promise(resolve => setTimeout(resolve, 100)) } // 确保文本不为空 @@ -114,6 +162,8 @@ export class TTSService { const latestSettings = store.getState().settings const serviceType = latestSettings.ttsServiceType || 'openai' console.log('使用的TTS服务类型:', serviceType) + // 记录当前使用的服务类型 + this.playingServiceType = serviceType console.log('当前TTS设置详情:', { ttsServiceType: serviceType, ttsEdgeVoice: latestSettings.ttsEdgeVoice, @@ -161,7 +211,8 @@ export class TTSService { } }) - this.isPlaying = true + // 更新播放状态 + this.updatePlayingState(true) console.log('开始播放TTS音频') // 释放URL对象 @@ -172,22 +223,10 @@ export class TTSService { const isEdgeTTS = serviceType === 'edge' const isSmallBlob = audioBlob.size < 100 - // 如果是浏览器TTS直接播放,则等待当前语音合成结束 - if (isEdgeTTS && isSmallBlob) { - // 检查全局变量中的当前语音合成状态 - // 如果还在播放,则不重置播放状态 - // 注意:这里我们无法直接访问 EdgeTTSService 中的 currentUtterance - // 所以我们使用定时器来检查语音合成是否完成 - console.log('浏览器TTS直接播放中,等待语音合成结束') - // 保持播放状态,直到语音合成结束 - // 使用定时器来检查语音合成是否完成 - // 大多数语音合成应该在几秒内完成 - setTimeout(() => { - this.isPlaying = false - console.log('浏览器TTS直接播放完成') - }, 10000) // 10秒后自动重置状态 - } else { - this.isPlaying = false + // 对于非EdgeTTS服务,直接更新状态 + // EdgeTTS服务的状态更新由定时器和edgeTTSComplete事件处理 + if (!(isEdgeTTS && isSmallBlob)) { + this.updatePlayingState(false) } } @@ -216,13 +255,17 @@ export class TTSService { if (this.audioElement) { this.audioElement.pause() this.audioElement.currentTime = 0 - this.isPlaying = false console.log('强制停止TTS播放') - - // 触发自定义事件,通知其他组件TTS已停止 - const event = new CustomEvent('tts-state-change', { detail: { isPlaying: false } }) - window.dispatchEvent(event) } + + // 如果是EdgeTTS,确保Web Speech API也停止 + if ('speechSynthesis' in window) { + window.speechSynthesis.cancel() + console.log('停止Web Speech API播放') + } + + // 更新状态并触发事件 + this.updatePlayingState(false) } /** diff --git a/src/renderer/src/services/tts/TTSServiceInterface.ts b/src/renderer/src/services/tts/TTSServiceInterface.ts index 66ffe88eb0..48b7c31bb3 100644 --- a/src/renderer/src/services/tts/TTSServiceInterface.ts +++ b/src/renderer/src/services/tts/TTSServiceInterface.ts @@ -9,4 +9,15 @@ export interface TTSServiceInterface { * @returns 返回音频Blob对象的Promise */ synthesize(text: string): Promise + + /** + * 流式合成语音 (可选实现) + * @param text 要合成的文本 + * @param onStart 开始回调 + * @param onData 数据块回调 + * @param onEnd 结束回调 + * @param onError 错误回调 + * @returns 返回请求ID + */ + synthesizeStream?(text: string, onStart: () => void, onData: (audioChunk: AudioBuffer) => void, onEnd: () => void, onError: (error: Error) => void): Promise } diff --git a/temp.txt b/temp.txt new file mode 100644 index 0000000000..fa8cc48270 --- /dev/null +++ b/temp.txt @@ -0,0 +1,8 @@ + // 不再自动清除回调函数,允许持续接收语音识别结果 + // setTimeout(() => { + // // 发送重置命令,确保浏览器不会继续发送结果 + // ASRService.cancelRecording() + // + // // 清除ASRService中的回调函数,防止后续结果被处理 + // ASRService.resultCallback = null + // }, 2000) // 2秒后强制取消,作为安全措施 \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2335107ded..19315811d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3952,6 +3952,7 @@ __metadata: diff: "npm:^7.0.0" docx: "npm:^9.0.2" dotenv-cli: "npm:^7.4.2" + edge-tts-node: "npm:^1.5.7" electron: "npm:31.7.6" electron-builder: "npm:^24.13.3" electron-devtools-installer: "npm:^3.2.0" @@ -4439,6 +4440,17 @@ __metadata: languageName: node linkType: hard +"asn1.js@npm:^4.10.1": + version: 4.10.1 + resolution: "asn1.js@npm:4.10.1" + dependencies: + bn.js: "npm:^4.0.0" + inherits: "npm:^2.0.1" + minimalistic-assert: "npm:^1.0.0" + checksum: 10c0/afa7f3ab9e31566c80175a75b182e5dba50589dcc738aa485be42bdd787e2a07246a4b034d481861123cbe646a7656f318f4f1cad2e9e5e808a210d5d6feaa88 + languageName: node + linkType: hard + "asn1@npm:~0.2.3": version: 0.2.6 resolution: "asn1@npm:0.2.6" @@ -4520,7 +4532,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.7.3, axios@npm:^1.7.7": +"axios@npm:^1.5.0, axios@npm:^1.7.3, axios@npm:^1.7.7": version: 1.8.4 resolution: "axios@npm:1.8.4" dependencies: @@ -4678,6 +4690,20 @@ __metadata: languageName: node linkType: hard +"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": + version: 4.12.1 + resolution: "bn.js@npm:4.12.1" + checksum: 10c0/b7f37a0cd5e4b79142b6f4292d518b416be34ae55d6dd6b0f66f96550c8083a50ffbbf8bda8d0ab471158cb81aa74ea4ee58fe33c7802e4a30b13810e98df116 + languageName: node + linkType: hard + +"bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 10c0/bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa + languageName: node + linkType: hard + "body-parser@npm:^2.0.1": version: 2.1.0 resolution: "body-parser@npm:2.1.0" @@ -4730,6 +4756,13 @@ __metadata: languageName: node linkType: hard +"brorand@npm:^1.0.1, brorand@npm:^1.1.0": + version: 1.1.0 + resolution: "brorand@npm:1.1.0" + checksum: 10c0/6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 + languageName: node + linkType: hard + "browser-image-compression@npm:^2.0.2": version: 2.0.2 resolution: "browser-image-compression@npm:2.0.2" @@ -4739,6 +4772,72 @@ __metadata: languageName: node linkType: hard +"browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": + version: 1.2.0 + resolution: "browserify-aes@npm:1.2.0" + dependencies: + buffer-xor: "npm:^1.0.3" + cipher-base: "npm:^1.0.0" + create-hash: "npm:^1.1.0" + evp_bytestokey: "npm:^1.0.3" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/967f2ae60d610b7b252a4cbb55a7a3331c78293c94b4dd9c264d384ca93354c089b3af9c0dd023534efdc74ffbc82510f7ad4399cf82bc37bc07052eea485f18 + languageName: node + linkType: hard + +"browserify-cipher@npm:^1.0.1": + version: 1.0.1 + resolution: "browserify-cipher@npm:1.0.1" + dependencies: + browserify-aes: "npm:^1.0.4" + browserify-des: "npm:^1.0.0" + evp_bytestokey: "npm:^1.0.0" + checksum: 10c0/aa256dcb42bc53a67168bbc94ab85d243b0a3b56109dee3b51230b7d010d9b78985ffc1fb36e145c6e4db151f888076c1cfc207baf1525d3e375cbe8187fe27d + languageName: node + linkType: hard + +"browserify-des@npm:^1.0.0": + version: 1.0.2 + resolution: "browserify-des@npm:1.0.2" + dependencies: + cipher-base: "npm:^1.0.1" + des.js: "npm:^1.0.0" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.1.2" + checksum: 10c0/943eb5d4045eff80a6cde5be4e5fbb1f2d5002126b5a4789c3c1aae3cdddb1eb92b00fb92277f512288e5c6af330730b1dbabcf7ce0923e749e151fcee5a074d + languageName: node + linkType: hard + +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0": + version: 4.1.1 + resolution: "browserify-rsa@npm:4.1.1" + dependencies: + bn.js: "npm:^5.2.1" + randombytes: "npm:^2.1.0" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/b650ee1192e3d7f3d779edc06dd96ed8720362e72ac310c367b9d7fe35f7e8dbb983c1829142b2b3215458be8bf17c38adc7224920843024ed8cf39e19c513c0 + languageName: node + linkType: hard + +"browserify-sign@npm:^4.2.3": + version: 4.2.3 + resolution: "browserify-sign@npm:4.2.3" + dependencies: + bn.js: "npm:^5.2.1" + browserify-rsa: "npm:^4.1.0" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + elliptic: "npm:^6.5.5" + hash-base: "npm:~3.0" + inherits: "npm:^2.0.4" + parse-asn1: "npm:^5.1.7" + readable-stream: "npm:^2.3.8" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/30c0eba3f5970a20866a4d3fbba2c5bd1928cd24f47faf995f913f1499214c6f3be14bb4d6ec1ab5c6cafb1eca9cb76ba1c2e1c04ed018370634d4e659c77216 + languageName: node + linkType: hard + "browserslist@npm:^4.21.1, browserslist@npm:^4.24.0": version: 4.24.4 resolution: "browserslist@npm:4.24.4" @@ -4812,6 +4911,13 @@ __metadata: languageName: node linkType: hard +"buffer-xor@npm:^1.0.3": + version: 1.0.3 + resolution: "buffer-xor@npm:1.0.3" + checksum: 10c0/fd269d0e0bf71ecac3146187cfc79edc9dbb054e2ee69b4d97dfb857c6d997c33de391696d04bdd669272751fa48e7872a22f3a6c7b07d6c0bc31dbe02a4075c + languageName: node + linkType: hard + "buffer@npm:^5.1.0, buffer@npm:^5.2.0, buffer@npm:^5.2.1, buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" @@ -5209,6 +5315,16 @@ __metadata: languageName: node linkType: hard +"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": + version: 1.0.6 + resolution: "cipher-base@npm:1.0.6" + dependencies: + inherits: "npm:^2.0.4" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/f73268e0ee6585800875d9748f2a2377ae7c2c3375cba346f75598ac6f6bc3a25dec56e984a168ced1a862529ffffe615363f750c40349039d96bd30fba0fca8 + languageName: node + linkType: hard + "classcat@npm:^5.0.3": version: 5.0.5 resolution: "classcat@npm:5.0.5" @@ -5601,6 +5717,43 @@ __metadata: languageName: node linkType: hard +"create-ecdh@npm:^4.0.4": + version: 4.0.4 + resolution: "create-ecdh@npm:4.0.4" + dependencies: + bn.js: "npm:^4.1.0" + elliptic: "npm:^6.5.3" + checksum: 10c0/77b11a51360fec9c3bce7a76288fc0deba4b9c838d5fb354b3e40c59194d23d66efe6355fd4b81df7580da0661e1334a235a2a5c040b7569ba97db428d466e7f + languageName: node + linkType: hard + +"create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0": + version: 1.2.0 + resolution: "create-hash@npm:1.2.0" + dependencies: + cipher-base: "npm:^1.0.1" + inherits: "npm:^2.0.1" + md5.js: "npm:^1.3.4" + ripemd160: "npm:^2.0.1" + sha.js: "npm:^2.4.0" + checksum: 10c0/d402e60e65e70e5083cb57af96d89567954d0669e90550d7cec58b56d49c4b193d35c43cec8338bc72358198b8cbf2f0cac14775b651e99238e1cf411490f915 + languageName: node + linkType: hard + +"create-hmac@npm:^1.1.4, create-hmac@npm:^1.1.7": + version: 1.1.7 + resolution: "create-hmac@npm:1.1.7" + dependencies: + cipher-base: "npm:^1.0.3" + create-hash: "npm:^1.1.0" + inherits: "npm:^2.0.1" + ripemd160: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + sha.js: "npm:^2.4.8" + checksum: 10c0/24332bab51011652a9a0a6d160eed1e8caa091b802335324ae056b0dcb5acbc9fcf173cf10d128eba8548c3ce98dfa4eadaa01bd02f44a34414baee26b651835 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -5619,6 +5772,26 @@ __metadata: languageName: node linkType: hard +"crypto-browserify@npm:^3.12.0": + version: 3.12.1 + resolution: "crypto-browserify@npm:3.12.1" + dependencies: + browserify-cipher: "npm:^1.0.1" + browserify-sign: "npm:^4.2.3" + create-ecdh: "npm:^4.0.4" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + diffie-hellman: "npm:^5.0.3" + hash-base: "npm:~3.0.4" + inherits: "npm:^2.0.4" + pbkdf2: "npm:^3.1.2" + public-encrypt: "npm:^4.0.3" + randombytes: "npm:^2.1.0" + randomfill: "npm:^1.0.4" + checksum: 10c0/184a2def7b16628e79841243232ab5497f18d8e158ac21b7ce90ab172427d0a892a561280adc08f9d4d517bce8db2a5b335dc21abb970f787f8e874bd7b9db7d + languageName: node + linkType: hard + "css-box-model@npm:^1.2.1": version: 1.2.1 resolution: "css-box-model@npm:1.2.1" @@ -6078,6 +6251,16 @@ __metadata: languageName: node linkType: hard +"des.js@npm:^1.0.0": + version: 1.1.0 + resolution: "des.js@npm:1.1.0" + dependencies: + inherits: "npm:^2.0.1" + minimalistic-assert: "npm:^1.0.0" + checksum: 10c0/671354943ad67493e49eb4c555480ab153edd7cee3a51c658082fcde539d2690ed2a4a0b5d1f401f9cde822edf3939a6afb2585f32c091f2d3a1b1665cd45236 + languageName: node + linkType: hard + "destroy@npm:^1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -6149,6 +6332,17 @@ __metadata: languageName: node linkType: hard +"diffie-hellman@npm:^5.0.3": + version: 5.0.3 + resolution: "diffie-hellman@npm:5.0.3" + dependencies: + bn.js: "npm:^4.1.0" + miller-rabin: "npm:^4.0.0" + randombytes: "npm:^2.0.0" + checksum: 10c0/ce53ccafa9ca544b7fc29b08a626e23a9b6562efc2a98559a0c97b4718937cebaa9b5d7d0a05032cc9c1435e9b3c1532b9e9bf2e0ede868525922807ad6e1ecf + languageName: node + linkType: hard + "dingbat-to-unicode@npm:^1.0.1": version: 1.0.1 resolution: "dingbat-to-unicode@npm:1.0.1" @@ -6383,6 +6577,22 @@ __metadata: languageName: node linkType: hard +"edge-tts-node@npm:^1.5.7": + version: 1.5.7 + resolution: "edge-tts-node@npm:1.5.7" + dependencies: + axios: "npm:^1.5.0" + buffer: "npm:^6.0.3" + crypto-browserify: "npm:^3.12.0" + isomorphic-ws: "npm:^5.0.0" + process: "npm:^0.11.10" + randombytes: "npm:^2.1.0" + stream-browserify: "npm:^3.0.0" + ws: "npm:^8.14.1" + checksum: 10c0/83b5df1d5312163006643fb6e6a9ca37ca6bc4c871b66f0c800f3e0bb1b2473fa0d67c125b256e2d93b493b8fbf59aaab06f60d4fa15fe3b7e2d3fb796016b1f + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -6548,6 +6758,21 @@ __metadata: languageName: node linkType: hard +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": + version: 6.6.1 + resolution: "elliptic@npm:6.6.1" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10c0/8b24ef782eec8b472053793ea1e91ae6bee41afffdfcb78a81c0a53b191e715cbe1292aa07165958a9bbe675bd0955142560b1a007ffce7d6c765bcaf951a867 + languageName: node + linkType: hard + "emittery@npm:^1.0.3": version: 1.1.0 resolution: "emittery@npm:1.1.0" @@ -7304,6 +7529,17 @@ __metadata: languageName: node linkType: hard +"evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3": + version: 1.0.3 + resolution: "evp_bytestokey@npm:1.0.3" + dependencies: + md5.js: "npm:^1.3.4" + node-gyp: "npm:latest" + safe-buffer: "npm:^5.1.1" + checksum: 10c0/77fbe2d94a902a80e9b8f5a73dcd695d9c14899c5e82967a61b1fc6cbbb28c46552d9b127cff47c45fcf684748bdbcfa0a50410349109de87ceb4b199ef6ee99 + languageName: node + linkType: hard + "execa@npm:^8.0.1": version: 8.0.1 resolution: "execa@npm:8.0.1" @@ -8480,7 +8716,28 @@ __metadata: languageName: node linkType: hard -"hash.js@npm:^1.1.7": +"hash-base@npm:^3.0.0": + version: 3.1.0 + resolution: "hash-base@npm:3.1.0" + dependencies: + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.6.0" + safe-buffer: "npm:^5.2.0" + checksum: 10c0/663eabcf4173326fbb65a1918a509045590a26cc7e0964b754eef248d281305c6ec9f6b31cb508d02ffca383ab50028180ce5aefe013e942b44a903ac8dc80d0 + languageName: node + linkType: hard + +"hash-base@npm:~3.0, hash-base@npm:~3.0.4": + version: 3.0.5 + resolution: "hash-base@npm:3.0.5" + dependencies: + inherits: "npm:^2.0.4" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/6dc185b79bad9b6d525cd132a588e4215380fdc36fec6f7a8a58c5db8e3b642557d02ad9c367f5e476c7c3ad3ccffa3607f308b124e1ed80e3b80a1b254db61e + languageName: node + linkType: hard + +"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3, hash.js@npm:^1.1.7": version: 1.1.7 resolution: "hash.js@npm:1.1.7" dependencies: @@ -8704,6 +8961,17 @@ __metadata: languageName: node linkType: hard +"hmac-drbg@npm:^1.0.1": + version: 1.0.1 + resolution: "hmac-drbg@npm:1.0.1" + dependencies: + hash.js: "npm:^1.0.3" + minimalistic-assert: "npm:^1.0.0" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10c0/f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -9073,7 +9341,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -9441,6 +9709,15 @@ __metadata: languageName: node linkType: hard +"isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: 10c0/a058ac8b5e6efe9e46252cb0bc67fd325005d7216451d1a51238bc62d7da8486f828ef017df54ddf742e0fffcbe4b1bcc2a66cc115b027ed0180334cd18df252 + languageName: node + linkType: hard + "isstream@npm:~0.1.2": version: 0.1.2 resolution: "isstream@npm:0.1.2" @@ -10372,6 +10649,17 @@ __metadata: languageName: node linkType: hard +"md5.js@npm:^1.3.4": + version: 1.3.5 + resolution: "md5.js@npm:1.3.5" + dependencies: + hash-base: "npm:^3.0.0" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.1.2" + checksum: 10c0/b7bd75077f419c8e013fc4d4dada48be71882e37d69a44af65a2f2804b91e253441eb43a0614423a1c91bb830b8140b0dc906bc797245e2e275759584f4efcc5 + languageName: node + linkType: hard + "md5@npm:^2.3.0": version: 2.3.0 resolution: "md5@npm:2.3.0" @@ -11288,6 +11576,18 @@ __metadata: languageName: node linkType: hard +"miller-rabin@npm:^4.0.0": + version: 4.0.1 + resolution: "miller-rabin@npm:4.0.1" + dependencies: + bn.js: "npm:^4.0.0" + brorand: "npm:^1.0.1" + bin: + miller-rabin: bin/miller-rabin + checksum: 10c0/26b2b96f6e49dbcff7faebb78708ed2f5f9ae27ac8cbbf1d7c08f83cf39bed3d418c0c11034dce997da70d135cc0ff6f3a4c15dc452f8e114c11986388a64346 + languageName: node + linkType: hard + "mime-db@npm:1.52.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" @@ -11405,13 +11705,20 @@ __metadata: languageName: node linkType: hard -"minimalistic-assert@npm:^1.0.1": +"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd languageName: node linkType: hard +"minimalistic-crypto-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-crypto-utils@npm:1.0.1" + checksum: 10c0/790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -12473,6 +12780,20 @@ __metadata: languageName: node linkType: hard +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": + version: 5.1.7 + resolution: "parse-asn1@npm:5.1.7" + dependencies: + asn1.js: "npm:^4.10.1" + browserify-aes: "npm:^1.2.0" + evp_bytestokey: "npm:^1.0.3" + hash-base: "npm:~3.0" + pbkdf2: "npm:^3.1.2" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/05eb5937405c904eb5a7f3633bab1acc11f4ae3478a07ef5c6d81ce88c3c0e505ff51f9c7b935ebc1265c868343793698fc91025755a895d0276f620f95e8a82 + languageName: node + linkType: hard + "parse-bmfont-ascii@npm:^1.0.3": version: 1.0.6 resolution: "parse-bmfont-ascii@npm:1.0.6" @@ -12661,6 +12982,19 @@ __metadata: languageName: node linkType: hard +"pbkdf2@npm:^3.1.2": + version: 3.1.2 + resolution: "pbkdf2@npm:3.1.2" + dependencies: + create-hash: "npm:^1.1.2" + create-hmac: "npm:^1.1.4" + ripemd160: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + sha.js: "npm:^2.4.8" + checksum: 10c0/5a30374e87d33fa080a92734d778cf172542cc7e41b96198c4c88763997b62d7850de3fbda5c3111ddf79805ee7c1da7046881c90ac4920b5e324204518b05fd + languageName: node + linkType: hard + "pdf-parse@npm:1.1.1": version: 1.1.1 resolution: "pdf-parse@npm:1.1.1" @@ -13054,6 +13388,20 @@ __metadata: languageName: node linkType: hard +"public-encrypt@npm:^4.0.3": + version: 4.0.3 + resolution: "public-encrypt@npm:4.0.3" + dependencies: + bn.js: "npm:^4.1.0" + browserify-rsa: "npm:^4.0.0" + create-hash: "npm:^1.1.0" + parse-asn1: "npm:^5.0.0" + randombytes: "npm:^2.0.1" + safe-buffer: "npm:^5.1.2" + checksum: 10c0/6c2cc19fbb554449e47f2175065d6b32f828f9b3badbee4c76585ac28ae8641aafb9bb107afc430c33c5edd6b05dbe318df4f7d6d7712b1093407b11c4280700 + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.2 resolution: "pump@npm:3.0.2" @@ -13138,6 +13486,25 @@ __metadata: languageName: node linkType: hard +"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: "npm:^5.1.0" + checksum: 10c0/50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 + languageName: node + linkType: hard + +"randomfill@npm:^1.0.4": + version: 1.0.4 + resolution: "randomfill@npm:1.0.4" + dependencies: + randombytes: "npm:^2.0.5" + safe-buffer: "npm:^5.1.0" + checksum: 10c0/11aeed35515872e8f8a2edec306734e6b74c39c46653607f03c68385ab8030e2adcc4215f76b5e4598e028c4750d820afd5c65202527d831d2a5f207fe2bc87c + languageName: node + linkType: hard + "range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": version: 1.2.1 resolution: "range-parser@npm:1.2.1" @@ -13925,7 +14292,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -13936,7 +14303,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.6, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.0, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.6, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.0, readable-stream@npm:^2.3.5, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -14416,6 +14783,16 @@ __metadata: languageName: node linkType: hard +"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": + version: 2.0.2 + resolution: "ripemd160@npm:2.0.2" + dependencies: + hash-base: "npm:^3.0.0" + inherits: "npm:^2.0.1" + checksum: 10c0/f6f0df78817e78287c766687aed4d5accbebc308a8e7e673fb085b9977473c1f139f0c5335d353f172a915bb288098430755d2ad3c4f30612f4dd0c901cd2c3a + languageName: node + linkType: hard + "roarr@npm:^2.15.3": version: 2.15.4 resolution: "roarr@npm:2.15.4" @@ -14551,7 +14928,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -14747,6 +15124,18 @@ __metadata: languageName: node linkType: hard +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": + version: 2.4.11 + resolution: "sha.js@npm:2.4.11" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + bin: + sha.js: ./bin.js + checksum: 10c0/b7a371bca8821c9cc98a0aeff67444a03d48d745cb103f17228b96793f455f0eb0a691941b89ea1e60f6359207e36081d9be193252b0f128e0daf9cfea2815a5 + languageName: node + linkType: hard + "shallowequal@npm:1.1.0": version: 1.1.0 resolution: "shallowequal@npm:1.1.0" @@ -15135,6 +15524,16 @@ __metadata: languageName: node linkType: hard +"stream-browserify@npm:^3.0.0": + version: 3.0.0 + resolution: "stream-browserify@npm:3.0.0" + dependencies: + inherits: "npm:~2.0.4" + readable-stream: "npm:^3.5.0" + checksum: 10c0/ec3b975a4e0aa4b3dc5e70ffae3fc8fd29ac725353a14e72f213dff477b00330140ad014b163a8cbb9922dfe90803f81a5ea2b269e1bbfd8bd71511b88f889ad + languageName: node + linkType: hard + "stream-head@npm:^3.0.0": version: 3.0.0 resolution: "stream-head@npm:3.0.0" @@ -16670,7 +17069,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.18.0": +"ws@npm:^8.13.0, ws@npm:^8.14.1, ws@npm:^8.18.0": version: 8.18.1 resolution: "ws@npm:8.18.1" peerDependencies: