mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 11:19:10 +08:00
添加了 TTS 相关服务并更新了设置
This commit is contained in:
parent
42c38b73ce
commit
808e5ef076
@ -16,32 +16,35 @@ export function registerMsTTSIpcHandlers(): void {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 流式合成语音
|
// 流式合成语音
|
||||||
ipcMain.handle(IpcChannel.MsTTS_SynthesizeStream, async (event, requestId: string, text: string, voice: string, outputFormat: string) => {
|
ipcMain.handle(
|
||||||
const window = BrowserWindow.fromWebContents(event.sender)
|
IpcChannel.MsTTS_SynthesizeStream,
|
||||||
if (!window) return
|
async (event, requestId: string, text: string, voice: string, outputFormat: string) => {
|
||||||
|
const window = BrowserWindow.fromWebContents(event.sender)
|
||||||
|
if (!window) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await MsTTSService.synthesizeStream(
|
await MsTTSService.synthesizeStream(
|
||||||
text,
|
text,
|
||||||
voice,
|
voice,
|
||||||
outputFormat,
|
outputFormat,
|
||||||
(chunk: Uint8Array) => {
|
(chunk: Uint8Array) => {
|
||||||
// 发送音频数据块
|
// 发送音频数据块
|
||||||
if (!window.isDestroyed()) {
|
if (!window.isDestroyed()) {
|
||||||
window.webContents.send(IpcChannel.MsTTS_StreamData, requestId, chunk)
|
window.webContents.send(IpcChannel.MsTTS_StreamData, requestId, chunk)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// 发送流结束信号
|
||||||
|
if (!window.isDestroyed()) {
|
||||||
|
window.webContents.send(IpcChannel.MsTTS_StreamEnd, requestId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
() => {
|
return { success: true }
|
||||||
// 发送流结束信号
|
} catch (error) {
|
||||||
if (!window.isDestroyed()) {
|
console.error('流式TTS合成失败:', error)
|
||||||
window.webContents.send(IpcChannel.MsTTS_StreamEnd, requestId)
|
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
return { success: true }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('流式TTS合成失败:', error)
|
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { MsEdgeTTS, OUTPUT_FORMAT } from 'edge-tts-node' // 新版支持流式的TTS库
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { EdgeTTS } from 'node-edge-tts' // 旧版TTS库
|
import { EdgeTTS } from 'node-edge-tts' // 旧版TTS库
|
||||||
import { MsEdgeTTS, OUTPUT_FORMAT } from 'edge-tts-node' // 新版支持流式的TTS库
|
|
||||||
|
|
||||||
// --- START OF HARDCODED VOICE LIST ---
|
// --- START OF HARDCODED VOICE LIST ---
|
||||||
// WARNING: This list is static and may become outdated.
|
// WARNING: This list is static and may become outdated.
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const ASRButton: FC<Props> = ({ onTranscribed, disabled = false, style }) => {
|
|||||||
try {
|
try {
|
||||||
// 添加事件监听器,监听服务器发送的stopped消息
|
// 添加事件监听器,监听服务器发送的stopped消息
|
||||||
const originalCallback = ASRService.resultCallback
|
const originalCallback = ASRService.resultCallback
|
||||||
const stopCallback = (text: string, isFinal?: boolean) => {
|
const stopCallback = (text: string) => {
|
||||||
// 如果是空字符串,只重置状态,不调用原始回调
|
// 如果是空字符串,只重置状态,不调用原始回调
|
||||||
if (text === '') {
|
if (text === '') {
|
||||||
setIsProcessing(false)
|
setIsProcessing(false)
|
||||||
|
|||||||
@ -289,7 +289,9 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
await VoiceCallService.startCall({
|
await VoiceCallService.startCall({
|
||||||
onTranscript: setTranscript,
|
onTranscript: setTranscript,
|
||||||
onResponse: () => { /* 响应在聊天界面处理 */ },
|
onResponse: () => {
|
||||||
|
/* 响应在聊天界面处理 */
|
||||||
|
},
|
||||||
onListeningStateChange: setIsListening,
|
onListeningStateChange: setIsListening,
|
||||||
onSpeakingStateChange: setIsSpeaking
|
onSpeakingStateChange: setIsSpeaking
|
||||||
})
|
})
|
||||||
@ -402,7 +404,7 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
await VoiceCallService.cancelRecording()
|
await VoiceCallService.cancelRecording()
|
||||||
setTranscript('')
|
setTranscript('')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('取消录音出错:', error);
|
console.error('取消录音出错:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => setIsProcessing(false), 500)
|
setTimeout(() => setIsProcessing(false), 500)
|
||||||
}
|
}
|
||||||
@ -412,23 +414,31 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
// --- 语音通话控制函数结束 ---
|
// --- 语音通话控制函数结束 ---
|
||||||
|
|
||||||
|
|
||||||
// --- 快捷键相关函数 ---
|
// --- 快捷键相关函数 ---
|
||||||
const getKeyDisplayName = (keyCode: string) => {
|
const getKeyDisplayName = (keyCode: string) => {
|
||||||
const keyMap: Record<string, string> = {
|
const keyMap: Record<string, string> = {
|
||||||
Space: '空格键', Enter: '回车键', ShiftLeft: '左Shift键', ShiftRight: '右Shift键',
|
Space: '空格键',
|
||||||
ControlLeft: '左Ctrl键', ControlRight: '右Ctrl键', AltLeft: '左Alt键', AltRight: '右Alt键'
|
Enter: '回车键',
|
||||||
|
ShiftLeft: '左Shift键',
|
||||||
|
ShiftRight: '右Shift键',
|
||||||
|
ControlLeft: '左Ctrl键',
|
||||||
|
ControlRight: '右Ctrl键',
|
||||||
|
AltLeft: '左Alt键',
|
||||||
|
AltRight: '右Alt键'
|
||||||
}
|
}
|
||||||
return keyMap[keyCode] || keyCode
|
return keyMap[keyCode] || keyCode
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShortcutKeyChange = useCallback((e: KeyboardEvent) => {
|
const handleShortcutKeyChange = useCallback(
|
||||||
e.preventDefault()
|
(e: KeyboardEvent) => {
|
||||||
if (isRecordingShortcut) {
|
e.preventDefault()
|
||||||
setTempShortcutKey(e.code)
|
if (isRecordingShortcut) {
|
||||||
setIsRecordingShortcut(false)
|
setTempShortcutKey(e.code)
|
||||||
}
|
setIsRecordingShortcut(false)
|
||||||
}, [isRecordingShortcut])
|
}
|
||||||
|
},
|
||||||
|
[isRecordingShortcut]
|
||||||
|
)
|
||||||
|
|
||||||
const saveShortcutKey = useCallback(() => {
|
const saveShortcutKey = useCallback(() => {
|
||||||
setShortcutKey(tempShortcutKey)
|
setShortcutKey(tempShortcutKey)
|
||||||
@ -437,31 +447,42 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
}, [tempShortcutKey])
|
}, [tempShortcutKey])
|
||||||
|
|
||||||
// 现在可以安全地使用 handleRecordStart/End
|
// 现在可以安全地使用 handleRecordStart/End
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
const handleKeyDown = useCallback(
|
||||||
if (isRecordingShortcut) {
|
(e: KeyboardEvent) => {
|
||||||
handleShortcutKeyChange(e)
|
if (isRecordingShortcut) {
|
||||||
return
|
handleShortcutKeyChange(e)
|
||||||
}
|
return
|
||||||
if (e.code === shortcutKey && !isProcessing && !isPaused && visible && !isShortcutPressed) {
|
}
|
||||||
e.preventDefault()
|
if (e.code === shortcutKey && !isProcessing && !isPaused && visible && !isShortcutPressed) {
|
||||||
setIsShortcutPressed(true)
|
e.preventDefault()
|
||||||
const mockEvent = new MouseEvent('mousedown') as unknown as React.MouseEvent // 类型断言
|
setIsShortcutPressed(true)
|
||||||
handleRecordStart(mockEvent) // 现在 handleRecordStart 已经定义
|
const mockEvent = new MouseEvent('mousedown') as unknown as React.MouseEvent // 类型断言
|
||||||
}
|
handleRecordStart(mockEvent) // 现在 handleRecordStart 已经定义
|
||||||
}, [
|
}
|
||||||
shortcutKey, isProcessing, isPaused, visible, isShortcutPressed,
|
},
|
||||||
handleRecordStart, // 依赖项
|
[
|
||||||
isRecordingShortcut, handleShortcutKeyChange
|
shortcutKey,
|
||||||
])
|
isProcessing,
|
||||||
|
isPaused,
|
||||||
|
visible,
|
||||||
|
isShortcutPressed,
|
||||||
|
handleRecordStart, // 依赖项
|
||||||
|
isRecordingShortcut,
|
||||||
|
handleShortcutKeyChange
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
const handleKeyUp = useCallback(
|
||||||
if (e.code === shortcutKey && isShortcutPressed && visible) {
|
(e: KeyboardEvent) => {
|
||||||
e.preventDefault()
|
if (e.code === shortcutKey && isShortcutPressed && visible) {
|
||||||
setIsShortcutPressed(false)
|
e.preventDefault()
|
||||||
const mockEvent = new MouseEvent('mouseup') as unknown as React.MouseEvent // 类型断言
|
setIsShortcutPressed(false)
|
||||||
handleRecordEnd(mockEvent) // 现在 handleRecordEnd 已经定义
|
const mockEvent = new MouseEvent('mouseup') as unknown as React.MouseEvent // 类型断言
|
||||||
}
|
handleRecordEnd(mockEvent) // 现在 handleRecordEnd 已经定义
|
||||||
}, [shortcutKey, isShortcutPressed, visible, handleRecordEnd]) // 依赖项
|
}
|
||||||
|
},
|
||||||
|
[shortcutKey, isShortcutPressed, visible, handleRecordEnd]
|
||||||
|
) // 依赖项
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedShortcut = localStorage.getItem('voiceCallShortcutKey')
|
const savedShortcut = localStorage.getItem('voiceCallShortcutKey')
|
||||||
@ -483,7 +504,6 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
}, [visible, handleKeyDown, handleKeyUp])
|
}, [visible, handleKeyDown, handleKeyUp])
|
||||||
// --- 快捷键相关函数结束 ---
|
// --- 快捷键相关函数结束 ---
|
||||||
|
|
||||||
|
|
||||||
// 如果不可见,直接返回 null
|
// 如果不可见,直接返回 null
|
||||||
if (!visible) return null
|
if (!visible) return null
|
||||||
|
|
||||||
@ -511,10 +531,14 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
{isSettingsVisible && (
|
{isSettingsVisible && (
|
||||||
<SettingsPanel> {/* 使用 styled-component */}
|
<SettingsPanel>
|
||||||
|
{' '}
|
||||||
|
{/* 使用 styled-component */}
|
||||||
<SettingsTitle>{t('voice_call.shortcut_key_setting')}</SettingsTitle> {/* 使用 styled-component */}
|
<SettingsTitle>{t('voice_call.shortcut_key_setting')}</SettingsTitle> {/* 使用 styled-component */}
|
||||||
<Space>
|
<Space>
|
||||||
<ShortcutKeyButton onClick={() => setIsRecordingShortcut(true)}> {/* 使用 styled-component */}
|
<ShortcutKeyButton onClick={() => setIsRecordingShortcut(true)}>
|
||||||
|
{' '}
|
||||||
|
{/* 使用 styled-component */}
|
||||||
{isRecordingShortcut ? t('voice_call.press_any_key') : getKeyDisplayName(tempShortcutKey)}
|
{isRecordingShortcut ? t('voice_call.press_any_key') : getKeyDisplayName(tempShortcutKey)}
|
||||||
</ShortcutKeyButton>
|
</ShortcutKeyButton>
|
||||||
<Button type="primary" onClick={saveShortcutKey}>
|
<Button type="primary" onClick={saveShortcutKey}>
|
||||||
@ -522,7 +546,9 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setIsSettingsVisible(false)}>{t('voice_call.cancel')}</Button>
|
<Button onClick={() => setIsSettingsVisible(false)}>{t('voice_call.cancel')}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
<SettingsTip> {/* 使用 styled-component */}
|
<SettingsTip>
|
||||||
|
{' '}
|
||||||
|
{/* 使用 styled-component */}
|
||||||
{t('voice_call.shortcut_key_tip')}
|
{t('voice_call.shortcut_key_tip')}
|
||||||
</SettingsTip>
|
</SettingsTip>
|
||||||
</SettingsPanel>
|
</SettingsPanel>
|
||||||
@ -579,4 +605,4 @@ const DraggableVoiceCallWindow: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DraggableVoiceCallWindow
|
export default DraggableVoiceCallWindow
|
||||||
|
|||||||
@ -15,7 +15,12 @@
|
|||||||
"initialization_failed": "Failed to initialize voice call",
|
"initialization_failed": "Failed to initialize voice call",
|
||||||
"error": "Voice call error",
|
"error": "Voice call error",
|
||||||
"initializing": "Initializing voice call...",
|
"initializing": "Initializing voice call...",
|
||||||
"ready": "Voice call ready"
|
"ready": "Voice call ready",
|
||||||
|
"shortcut_key_setting": "[to be translated]:语音识别快捷键设置",
|
||||||
|
"press_any_key": "[to be translated]:请按任意键...",
|
||||||
|
"save": "[to be translated]:保存",
|
||||||
|
"cancel": "[to be translated]:取消",
|
||||||
|
"shortcut_key_tip": "[to be translated]:按下此快捷键开始录音,松开快捷键结束录音并发送"
|
||||||
},
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "Add to Assistant",
|
"add.button": "Add to Assistant",
|
||||||
|
|||||||
@ -1421,8 +1421,7 @@
|
|||||||
"mstts.voice": "[to be translated]:免费在线 TTS音色",
|
"mstts.voice": "[to be translated]:免费在线 TTS音色",
|
||||||
"mstts.output_format": "[to be translated]:输出格式",
|
"mstts.output_format": "[to be translated]:输出格式",
|
||||||
"mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。",
|
"mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。",
|
||||||
"error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色",
|
"error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色"
|
||||||
"filter.emojis": "[to be translated]:过滤表情符号"
|
|
||||||
},
|
},
|
||||||
"asr": {
|
"asr": {
|
||||||
"title": "音声認識",
|
"title": "音声認識",
|
||||||
@ -1544,7 +1543,12 @@
|
|||||||
"initialization_failed": "[to be translated]:初始化语音通话失败",
|
"initialization_failed": "[to be translated]:初始化语音通话失败",
|
||||||
"error": "[to be translated]:语音通话出错",
|
"error": "[to be translated]:语音通话出错",
|
||||||
"initializing": "[to be translated]:正在初始化语音通话...",
|
"initializing": "[to be translated]:正在初始化语音通话...",
|
||||||
"ready": "[to be translated]:语音通话已就绪"
|
"ready": "[to be translated]:语音通话已就绪",
|
||||||
|
"shortcut_key_setting": "[to be translated]:语音识别快捷键设置",
|
||||||
|
"press_any_key": "[to be translated]:请按任意键...",
|
||||||
|
"save": "[to be translated]:保存",
|
||||||
|
"cancel": "[to be translated]:取消",
|
||||||
|
"shortcut_key_tip": "[to be translated]:按下此快捷键开始录音,松开快捷键结束录音并发送"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1421,8 +1421,7 @@
|
|||||||
"mstts.voice": "[to be translated]:免费在线 TTS音色",
|
"mstts.voice": "[to be translated]:免费在线 TTS音色",
|
||||||
"mstts.output_format": "[to be translated]:输出格式",
|
"mstts.output_format": "[to be translated]:输出格式",
|
||||||
"mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。",
|
"mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。",
|
||||||
"error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色",
|
"error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色"
|
||||||
"filter.emojis": "[to be translated]:过滤表情符号"
|
|
||||||
},
|
},
|
||||||
"voice": {
|
"voice": {
|
||||||
"title": "[to be translated]:语音功能",
|
"title": "[to be translated]:语音功能",
|
||||||
@ -1544,7 +1543,12 @@
|
|||||||
"initialization_failed": "[to be translated]:初始化语音通话失败",
|
"initialization_failed": "[to be translated]:初始化语音通话失败",
|
||||||
"error": "[to be translated]:语音通话出错",
|
"error": "[to be translated]:语音通话出错",
|
||||||
"initializing": "[to be translated]:正在初始化语音通话...",
|
"initializing": "[to be translated]:正在初始化语音通话...",
|
||||||
"ready": "[to be translated]:语音通话已就绪"
|
"ready": "[to be translated]:语音通话已就绪",
|
||||||
|
"shortcut_key_setting": "[to be translated]:语音识别快捷键设置",
|
||||||
|
"press_any_key": "[to be translated]:请按任意键...",
|
||||||
|
"save": "[to be translated]:保存",
|
||||||
|
"cancel": "[to be translated]:取消",
|
||||||
|
"shortcut_key_tip": "[to be translated]:按下此快捷键开始录音,松开快捷键结束录音并发送"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1421,8 +1421,7 @@
|
|||||||
"mstts.voice": "[to be translated]:免费在线 TTS音色",
|
"mstts.voice": "[to be translated]:免费在线 TTS音色",
|
||||||
"mstts.output_format": "[to be translated]:输出格式",
|
"mstts.output_format": "[to be translated]:输出格式",
|
||||||
"mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。",
|
"mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。",
|
||||||
"error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色",
|
"error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色"
|
||||||
"filter.emojis": "[to be translated]:过滤表情符号"
|
|
||||||
},
|
},
|
||||||
"voice": {
|
"voice": {
|
||||||
"title": "[to be translated]:语音功能",
|
"title": "[to be translated]:语音功能",
|
||||||
@ -1544,7 +1543,12 @@
|
|||||||
"initialization_failed": "[to be translated]:初始化语音通话失败",
|
"initialization_failed": "[to be translated]:初始化语音通话失败",
|
||||||
"error": "[to be translated]:语音通话出错",
|
"error": "[to be translated]:语音通话出错",
|
||||||
"initializing": "[to be translated]:正在初始化语音通话...",
|
"initializing": "[to be translated]:正在初始化语音通话...",
|
||||||
"ready": "[to be translated]:语音通话已就绪"
|
"ready": "[to be translated]:语音通话已就绪",
|
||||||
|
"shortcut_key_setting": "[to be translated]:语音识别快捷键设置",
|
||||||
|
"press_any_key": "[to be translated]:请按任意键...",
|
||||||
|
"save": "[to be translated]:保存",
|
||||||
|
"cancel": "[to be translated]:取消",
|
||||||
|
"shortcut_key_tip": "[to be translated]:按下此快捷键开始录音,松开快捷键结束录音并发送"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,7 +77,8 @@ let _files: FileType[] = []
|
|||||||
|
|
||||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => {
|
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => {
|
||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
const [asrCurrentText, setAsrCurrentText] = useState('')
|
// 用于存储语音识别的中间结果,不直接显示在输入框中
|
||||||
|
const [, setAsrCurrentText] = useState('')
|
||||||
const [inputFocus, setInputFocus] = useState(false)
|
const [inputFocus, setInputFocus] = useState(false)
|
||||||
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id)
|
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id)
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -155,13 +155,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果是最后一条助手消息,且消息状态为成功,且不是正在生成中,且TTS已启用
|
// 如果是最后一条助手消息,且消息状态为成功,且不是正在生成中,且TTS已启用
|
||||||
// 注意:只有在语音通话窗口打开时才自动播放TTS
|
// 注意:只有在语音通话窗口打开时才自动播放TTS
|
||||||
if (
|
if (isLastMessage && isAssistantMessage && message.status === 'success' && !generating && ttsEnabled) {
|
||||||
isLastMessage &&
|
|
||||||
isAssistantMessage &&
|
|
||||||
message.status === 'success' &&
|
|
||||||
!generating &&
|
|
||||||
ttsEnabled
|
|
||||||
) {
|
|
||||||
// 如果语音通话窗口没有打开,则不自动播放TTS
|
// 如果语音通话窗口没有打开,则不自动播放TTS
|
||||||
if (!isVoiceCallActive) {
|
if (!isVoiceCallActive) {
|
||||||
console.log('不自动播放TTS,因为语音通话窗口没有打开:', isVoiceCallActive)
|
console.log('不自动播放TTS,因为语音通话窗口没有打开:', isVoiceCallActive)
|
||||||
|
|||||||
@ -170,7 +170,8 @@ class ASRService {
|
|||||||
// 直接调用回调函数
|
// 直接调用回调函数
|
||||||
this.resultCallback(data.data.text, true)
|
this.resultCallback(data.data.text, true)
|
||||||
window.message.success({ content: i18n.t('settings.asr.success'), key: 'asr-processing' })
|
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)
|
console.log('[ASRService] 收到中间结果,调用回调函数,文本:', data.data.text)
|
||||||
this.resultCallback(data.data.text, false)
|
this.resultCallback(data.data.text, false)
|
||||||
|
|||||||
@ -6,10 +6,10 @@ export class AudioStreamProcessor {
|
|||||||
private audioContext: AudioContext | null = null
|
private audioContext: AudioContext | null = null
|
||||||
private audioQueue: Uint8Array[] = []
|
private audioQueue: Uint8Array[] = []
|
||||||
private isProcessing: boolean = false
|
private isProcessing: boolean = false
|
||||||
|
|
||||||
// 回调函数
|
// 回调函数
|
||||||
public onAudioBuffer: ((buffer: AudioBuffer) => void) | null = null
|
public onAudioBuffer: ((buffer: AudioBuffer) => void) | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化音频处理器
|
* 初始化音频处理器
|
||||||
*/
|
*/
|
||||||
@ -19,7 +19,7 @@ export class AudioStreamProcessor {
|
|||||||
this.audioQueue = []
|
this.audioQueue = []
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理音频数据块
|
* 处理音频数据块
|
||||||
* @param chunk 音频数据块
|
* @param chunk 音频数据块
|
||||||
@ -28,16 +28,16 @@ export class AudioStreamProcessor {
|
|||||||
if (!this.audioContext) {
|
if (!this.audioContext) {
|
||||||
throw new Error('AudioStreamProcessor not initialized')
|
throw new Error('AudioStreamProcessor not initialized')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将数据块添加到队列
|
// 将数据块添加到队列
|
||||||
this.audioQueue.push(chunk)
|
this.audioQueue.push(chunk)
|
||||||
|
|
||||||
// 如果没有正在处理,开始处理
|
// 如果没有正在处理,开始处理
|
||||||
if (!this.isProcessing) {
|
if (!this.isProcessing) {
|
||||||
this.processQueue()
|
this.processQueue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理队列中的音频数据
|
* 处理队列中的音频数据
|
||||||
*/
|
*/
|
||||||
@ -46,20 +46,18 @@ export class AudioStreamProcessor {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
// 获取队列中的第一个数据块
|
// 获取队列中的第一个数据块
|
||||||
const chunk = this.audioQueue.shift()!
|
const chunk = this.audioQueue.shift()!
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 解码音频数据
|
// 解码音频数据
|
||||||
// 将SharedArrayBuffer转换为ArrayBuffer
|
// 将SharedArrayBuffer转换为ArrayBuffer
|
||||||
const arrayBuffer = chunk.buffer instanceof SharedArrayBuffer
|
const arrayBuffer = chunk.buffer instanceof SharedArrayBuffer ? new Uint8Array(chunk.buffer).buffer : chunk.buffer
|
||||||
? new Uint8Array(chunk.buffer).buffer
|
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer as ArrayBuffer)
|
||||||
: chunk.buffer
|
|
||||||
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer)
|
|
||||||
|
|
||||||
// 调用回调函数
|
// 调用回调函数
|
||||||
if (this.onAudioBuffer) {
|
if (this.onAudioBuffer) {
|
||||||
this.onAudioBuffer(audioBuffer)
|
this.onAudioBuffer(audioBuffer)
|
||||||
@ -67,20 +65,20 @@ export class AudioStreamProcessor {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解码音频数据失败:', error)
|
console.error('解码音频数据失败:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续处理队列中的下一个数据块
|
// 继续处理队列中的下一个数据块
|
||||||
this.processQueue()
|
this.processQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 完成处理
|
* 完成处理
|
||||||
*/
|
*/
|
||||||
public async finish(): Promise<void> {
|
public async finish(): Promise<void> {
|
||||||
// 等待队列处理完成
|
// 等待队列处理完成
|
||||||
while (this.audioQueue.length > 0) {
|
while (this.audioQueue.length > 0) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭音频上下文
|
// 关闭音频上下文
|
||||||
if (this.audioContext) {
|
if (this.audioContext) {
|
||||||
await this.audioContext.close()
|
await this.audioContext.close()
|
||||||
|
|||||||
@ -63,13 +63,13 @@ export class TTSService {
|
|||||||
// 只有在使用EdgeTTS且标记为正在播放时才检查
|
// 只有在使用EdgeTTS且标记为正在播放时才检查
|
||||||
if (this.isPlaying && this.playingServiceType === 'edge') {
|
if (this.isPlaying && this.playingServiceType === 'edge') {
|
||||||
// 检查是否还在播放
|
// 检查是否还在播放
|
||||||
const isSpeaking = window.speechSynthesis.speaking;
|
const isSpeaking = window.speechSynthesis.speaking
|
||||||
if (!isSpeaking) {
|
if (!isSpeaking) {
|
||||||
console.log('检测到speechSynthesis不再播放,更新状态')
|
console.log('检测到speechSynthesis不再播放,更新状态')
|
||||||
this.updatePlayingState(false);
|
this.updatePlayingState(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 500); // 每500毫秒检查一次
|
}, 500) // 每500毫秒检查一次
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export class TTSService {
|
|||||||
private updatePlayingState(isPlaying: boolean): void {
|
private updatePlayingState(isPlaying: boolean): void {
|
||||||
// 只有状态变化时才更新和触发事件
|
// 只有状态变化时才更新和触发事件
|
||||||
if (this.isPlaying !== isPlaying) {
|
if (this.isPlaying !== isPlaying) {
|
||||||
this.isPlaying = isPlaying;
|
this.isPlaying = isPlaying
|
||||||
console.log(`TTS播放状态更新: ${isPlaying ? '开始播放' : '停止播放'}`)
|
console.log(`TTS播放状态更新: ${isPlaying ? '开始播放' : '停止播放'}`)
|
||||||
|
|
||||||
// 触发自定义事件,通知其他组件TTS状态变化
|
// 触发自定义事件,通知其他组件TTS状态变化
|
||||||
@ -118,11 +118,11 @@ export class TTSService {
|
|||||||
|
|
||||||
// 如果停止播放,清除服务类型
|
// 如果停止播放,清除服务类型
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
this.playingServiceType = null;
|
this.playingServiceType = null
|
||||||
|
|
||||||
// 确保Web Speech API也停止
|
// 确保Web Speech API也停止
|
||||||
if ('speechSynthesis' in window) {
|
if ('speechSynthesis' in window) {
|
||||||
window.speechSynthesis.cancel();
|
window.speechSynthesis.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +148,7 @@ export class TTSService {
|
|||||||
if (this.isPlaying) {
|
if (this.isPlaying) {
|
||||||
this.stop()
|
this.stop()
|
||||||
// 添加短暂延迟,确保上一个播放完全停止
|
// 添加短暂延迟,确保上一个播放完全停止
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保文本不为空
|
// 确保文本不为空
|
||||||
|
|||||||
@ -19,5 +19,11 @@ export interface TTSServiceInterface {
|
|||||||
* @param onError 错误回调
|
* @param onError 错误回调
|
||||||
* @returns 返回请求ID
|
* @returns 返回请求ID
|
||||||
*/
|
*/
|
||||||
synthesizeStream?(text: string, onStart: () => void, onData: (audioChunk: AudioBuffer) => void, onEnd: () => void, onError: (error: Error) => void): Promise<string>
|
synthesizeStream?(
|
||||||
|
text: string,
|
||||||
|
onStart: () => void,
|
||||||
|
onData: (audioChunk: AudioBuffer) => void,
|
||||||
|
onEnd: () => void,
|
||||||
|
onError: (error: Error) => void
|
||||||
|
): Promise<string>
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user