From 09a6633370696c81c01c6f9d04ab984e60c2246e Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 10 Apr 2025 13:48:29 +0800 Subject: [PATCH] refactor: Clean up code formatting and improve readability across multiple files - Standardized code formatting by removing unnecessary line breaks and ensuring consistent use of semicolons. - Enhanced readability in various components, including ASRButton, TTSButton, and TTSService, by restructuring code blocks and improving indentation. - Updated comments for clarity and consistency in ASRService and TTSService. - Adjusted import statements for better organization in several files, including TTSStopButton and ASRSettings. - Improved the handling of promises and asynchronous functions for better code flow. --- electron.vite.config.ts | 6 +- public/asr-server/server.js | 8 +- src/main/ipc.ts | 6 +- src/renderer/src/assets/asr-server/server.js | 24 +- src/renderer/src/components/ASRButton.tsx | 40 +- src/renderer/src/components/TTSButton.tsx | 10 +- .../src/pages/home/Inputbar/Inputbar.tsx | 28 +- .../pages/home/Messages/MessageMenubar.tsx | 39 +- .../src/pages/home/Messages/Messages.tsx | 1 - .../src/pages/home/Messages/TTSStopButton.tsx | 10 +- .../settings/TTSSettings/ASRSettings.tsx | 82 ++-- .../settings/TTSSettings/TTSSettings.tsx | 11 +- src/renderer/src/services/ASRService.ts | 19 +- src/renderer/src/services/TTSService.ts | 449 +++++++++++------- 14 files changed, 437 insertions(+), 296 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 3a52c4ddcb..2d81d04582 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -80,13 +80,13 @@ export default defineConfig({ build: { rollupOptions: { input: { - index: resolve('src/renderer/index.html'), - }, + index: resolve('src/renderer/index.html') + } }, // 复制ASR服务器文件 assetsInlineLimit: 0, // 确保复制assets目录下的所有文件 - copyPublicDir: true, + copyPublicDir: true } } }) diff --git a/public/asr-server/server.js b/public/asr-server/server.js index b9e4bec168..c09927780e 100644 --- a/public/asr-server/server.js +++ b/public/asr-server/server.js @@ -39,8 +39,8 @@ wss.on('connection', (ws) => { console.log('[Server] Browser identified and connected') // Notify Electron that the browser is ready if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { - electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })); - console.log('[Server] Sent browser_ready status to Electron'); + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent browser_ready status to Electron') } // Notify Electron if it's already connected if (electronConnection) { @@ -66,8 +66,8 @@ wss.on('connection', (ws) => { console.log('[Server] Electron identified and connected') // If browser is already connected when Electron connects, notify Electron immediately if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { - electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })); - console.log('[Server] Sent initial browser_ready status to Electron'); + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent initial browser_ready status to Electron') } ws.on('close', () => { console.log('[Server] Electron disconnected') diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 47c13b0ef7..752e2e5510 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,5 +1,5 @@ +import { ChildProcess, spawn } from 'node:child_process' import fs from 'node:fs' -import { spawn, ChildProcess } from 'node:child_process' import path from 'node:path' import { isMac, isWin } from '@main/constant' @@ -361,7 +361,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { }) // 等待一段时间确保服务器启动 - await new Promise(resolve => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 1000)) return { success: true, pid: asrServerProcess.pid } } catch (error) { @@ -386,7 +386,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { asrServerProcess.kill() // 等待一段时间确保进程已经退出 - await new Promise(resolve => setTimeout(resolve, 500)) + await new Promise((resolve) => setTimeout(resolve, 500)) asrServerProcess = null return { success: true } diff --git a/src/renderer/src/assets/asr-server/server.js b/src/renderer/src/assets/asr-server/server.js index 19479cfbbb..7b3d77d9f2 100644 --- a/src/renderer/src/assets/asr-server/server.js +++ b/src/renderer/src/assets/asr-server/server.js @@ -9,33 +9,33 @@ const port = 8080 // Define the port // 获取index.html文件的路径 function getIndexHtmlPath() { // 在开发环境中,直接使用相对路径 - const devPath = path.join(__dirname, 'index.html'); + const devPath = path.join(__dirname, 'index.html') // 在pkg打包后,文件会被包含在可执行文件中 // 使用process.pkg检测是否是打包环境 if (process.pkg) { // 在打包环境中,使用绝对路径 - return path.join(path.dirname(process.execPath), 'index.html'); + return path.join(path.dirname(process.execPath), 'index.html') } // 如果文件存在,返回开发路径 try { if (require('fs').existsSync(devPath)) { - return devPath; + return devPath } } catch (e) { - console.error('Error checking file existence:', e); + console.error('Error checking file existence:', e) } // 如果都不存在,尝试使用当前目录 - return path.join(process.cwd(), 'index.html'); + return path.join(process.cwd(), 'index.html') } // 提供网页给浏览器 app.get('/', (req, res) => { - const indexPath = getIndexHtmlPath(); - console.log(`Serving index.html from: ${indexPath}`); - res.sendFile(indexPath); + const indexPath = getIndexHtmlPath() + console.log(`Serving index.html from: ${indexPath}`) + res.sendFile(indexPath) }) const server = http.createServer(app) @@ -65,8 +65,8 @@ wss.on('connection', (ws) => { console.log('[Server] Browser identified and connected') // Notify Electron that the browser is ready if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { - electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })); - console.log('[Server] Sent browser_ready status to Electron'); + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent browser_ready status to Electron') } // Notify Electron if it's already connected if (electronConnection) { @@ -92,8 +92,8 @@ wss.on('connection', (ws) => { console.log('[Server] Electron identified and connected') // If browser is already connected when Electron connects, notify Electron immediately if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { - electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })); - console.log('[Server] Sent initial browser_ready status to Electron'); + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent initial browser_ready status to Electron') } ws.on('close', () => { console.log('[Server] Electron disconnected') diff --git a/src/renderer/src/components/ASRButton.tsx b/src/renderer/src/components/ASRButton.tsx index 67813a9af1..89ad0347d9 100644 --- a/src/renderer/src/components/ASRButton.tsx +++ b/src/renderer/src/components/ASRButton.tsx @@ -105,7 +105,14 @@ const ASRButton: FC = ({ onTranscribed, disabled = false, style }) => { } return ( - + = ({ onTranscribed, disabled = false, style }) => { onDoubleClick={handleCancel} disabled={disabled || isProcessing || (isCountingDown && countdown > 0)} style={style} - className={isCountingDown ? 'counting-down' : ''} - > - {isCountingDown && ( - {countdown} - )} + className={isCountingDown ? 'counting-down' : ''}> + {isCountingDown && {countdown}} {isCountingDown && ( @@ -151,9 +155,15 @@ const CountdownIndicator = styled.div` z-index: 10; @keyframes pulse { - 0% { opacity: 0.7; } - 50% { opacity: 1; } - 100% { opacity: 0.7; } + 0% { + opacity: 0.7; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.7; + } } &:after { @@ -176,9 +186,15 @@ const CountdownNumber = styled.span` animation: zoom 1s infinite; @keyframes zoom { - 0% { transform: scale(0.8); } - 50% { transform: scale(1.2); } - 100% { transform: scale(0.8); } + 0% { + transform: scale(0.8); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(0.8); + } } ` diff --git a/src/renderer/src/components/TTSButton.tsx b/src/renderer/src/components/TTSButton.tsx index 36b856a434..58e4c2039a 100644 --- a/src/renderer/src/components/TTSButton.tsx +++ b/src/renderer/src/components/TTSButton.tsx @@ -1,9 +1,9 @@ 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 { useTranslation } from 'react-i18next' -import TTSService from '@renderer/services/TTSService' -import { Message } from '@renderer/types' interface TTSButtonProps { message: Message @@ -24,7 +24,7 @@ const TTSButton: React.FC = ({ message, className }) => { setIsSpeaking(true) try { await TTSService.speakFromMessage(message) - + // 监听播放结束 const checkPlayingStatus = () => { if (!TTSService.isCurrentlyPlaying()) { @@ -32,9 +32,9 @@ const TTSButton: React.FC = ({ message, className }) => { clearInterval(checkInterval) } } - + const checkInterval = setInterval(checkPlayingStatus, 500) - + // 安全机制,确保即使出错也会重置状态 setTimeout(() => { if (isSpeaking) { diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index a6890bac0c..460e24eb66 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -13,8 +13,8 @@ import { ThunderboltOutlined, TranslationOutlined } from '@ant-design/icons' -import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel' import ASRButton from '@renderer/components/ASRButton' +import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel' import TranslateButton from '@renderer/components/TranslateButton' import { isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import db from '@renderer/databases' @@ -1009,19 +1009,21 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = - { - // 如果是空字符串,不做任何处理 - if (!transcribedText) return + { + // 如果是空字符串,不做任何处理 + if (!transcribedText) return - // 将识别的文本添加到当前输入框 - setText((prevText) => { - // 如果当前有文本,添加空格后再添加识别的文本 - if (prevText.trim()) { - return prevText + ' ' + transcribedText - } - return transcribedText - }) - }} /> + // 将识别的文本添加到当前输入框 + setText((prevText) => { + // 如果当前有文本,添加空格后再添加识别的文本 + if (prevText.trim()) { + return prevText + ' ' + transcribedText + } + return transcribedText + }) + }} + /> {loading && ( diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 0acbe80234..e5189732da 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -22,8 +22,8 @@ import { TranslateLanguageOptions } from '@renderer/config/translate' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService' -import TTSService from '@renderer/services/TTSService' import { translateText } from '@renderer/services/TranslateService' +import TTSService from '@renderer/services/TTSService' import { RootState } from '@renderer/store' import type { Message, Model } from '@renderer/types' import type { Assistant, Topic } from '@renderer/types' @@ -165,24 +165,31 @@ const MessageMenubar: FC = (props) => { content: content.trim(), metadata: { ...message.metadata, - generateImage: imageUrls.length > 0 ? { - type: 'url', - images: imageUrls - } : undefined + generateImage: + imageUrls.length > 0 + ? { + type: 'url', + images: imageUrls + } + : undefined } }) - resendMessage && handleResendUserMessage({ - ...message, - content: content.trim(), - metadata: { - ...message.metadata, - generateImage: imageUrls.length > 0 ? { - type: 'url', - images: imageUrls - } : undefined - } - }) + resendMessage && + handleResendUserMessage({ + ...message, + content: content.trim(), + metadata: { + ...message.metadata, + generateImage: + imageUrls.length > 0 + ? { + type: 'url', + images: imageUrls + } + : undefined + } + }) } }, [message, editMessage, handleResendUserMessage, t]) diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 4924023913..9bd50cacd9 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -3,7 +3,6 @@ import { LOAD_MORE_COUNT } from '@renderer/config/constant' import db from '@renderer/databases' import { useAssistant } from '@renderer/hooks/useAssistant' import { useMessageOperations, useTopicLoading, useTopicMessages } from '@renderer/hooks/useMessageOperations' - import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic' diff --git a/src/renderer/src/pages/home/Messages/TTSStopButton.tsx b/src/renderer/src/pages/home/Messages/TTSStopButton.tsx index 3b586f7e75..82b6f921eb 100644 --- a/src/renderer/src/pages/home/Messages/TTSStopButton.tsx +++ b/src/renderer/src/pages/home/Messages/TTSStopButton.tsx @@ -1,9 +1,9 @@ -import { useTranslation } from 'react-i18next' import { SoundOutlined } from '@ant-design/icons' -import { Tooltip } from 'antd' -import styled from 'styled-components' -import { useCallback, useEffect, useState } from 'react' import TTSService from '@renderer/services/TTSService' +import { Tooltip } from 'antd' +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' const TTSStopButton: React.FC = () => { const { t } = useTranslation() @@ -27,7 +27,7 @@ const TTSStopButton: React.FC = () => { TTSService.stop() // 等待一下,确保播放已经完全停止 - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise((resolve) => setTimeout(resolve, 100)) // 再次检查并停止,确保强制停止 if (TTSService.isCurrentlyPlaying()) { diff --git a/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx index 321f8f4e9e..3c1c122b3a 100644 --- a/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx +++ b/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx @@ -1,24 +1,16 @@ -import { InfoCircleOutlined, GlobalOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons' -import { useTheme } from '@renderer/context/ThemeProvider' -import ASRService from '@renderer/services/ASRService' +import { GlobalOutlined, InfoCircleOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons' import ASRServerService from '@renderer/services/ASRServerService' +import ASRService from '@renderer/services/ASRService' import { useAppDispatch } from '@renderer/store' -import { - setAsrApiKey, - setAsrApiUrl, - setAsrEnabled, - setAsrModel, - setAsrServiceType -} from '@renderer/store/settings' +import { setAsrApiKey, setAsrApiUrl, setAsrEnabled, setAsrModel, setAsrServiceType } from '@renderer/store/settings' import { Button, Form, Input, Select, Space, Switch } from 'antd' -import { FC, useState, useEffect } from 'react' +import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import styled from 'styled-components' const ASRSettings: FC = () => { const { t } = useTranslation() - const { isDark } = useTheme() const dispatch = useAppDispatch() // 服务器状态 @@ -47,9 +39,7 @@ const ASRSettings: FC = () => { ] // 模型选项 - const modelOptions = [ - { label: 'whisper-1', value: 'whisper-1' } - ] + const modelOptions = [{ label: 'whisper-1', value: 'whisper-1' }] return ( @@ -137,8 +127,7 @@ const ASRSettings: FC = () => { setIsServerRunning(true) } }} - disabled={!asrEnabled || isServerRunning} - > + disabled={!asrEnabled || isServerRunning}> {t('settings.asr.server.start')} @@ -160,27 +148,33 @@ const ASRSettings: FC = () => { type="primary" icon={} onClick={() => ASRServerService.openServerPage()} - disabled={!asrEnabled || !isServerRunning} - > + disabled={!asrEnabled || !isServerRunning}> {t('settings.asr.open_browser')} @@ -239,27 +233,27 @@ const Alert = styled.div<{ type: 'info' | 'warning' | 'error' | 'success' }>` props.type === 'info' ? 'var(--color-info-bg)' : props.type === 'warning' - ? 'var(--color-warning-bg)' - : props.type === 'error' - ? 'var(--color-error-bg)' - : 'var(--color-success-bg)'}; + ? 'var(--color-warning-bg)' + : props.type === 'error' + ? 'var(--color-error-bg)' + : 'var(--color-success-bg)'}; border: 1px solid ${(props) => props.type === 'info' ? 'var(--color-info-border)' : props.type === 'warning' - ? 'var(--color-warning-border)' - : props.type === 'error' - ? 'var(--color-error-border)' - : 'var(--color-success-border)'}; + ? 'var(--color-warning-border)' + : props.type === 'error' + ? 'var(--color-error-border)' + : 'var(--color-success-border)'}; color: ${(props) => props.type === 'info' ? 'var(--color-info-text)' : props.type === 'warning' - ? 'var(--color-warning-text)' - : props.type === 'error' - ? 'var(--color-error-text)' - : 'var(--color-success-text)'}; + ? 'var(--color-warning-text)' + : props.type === 'error' + ? 'var(--color-error-text)' + : 'var(--color-success-text)'}; ` const BrowserTip = styled.div` diff --git a/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx index 29b01c12f0..76458971be 100644 --- a/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx +++ b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx @@ -17,7 +17,7 @@ import { setTtsServiceType, setTtsVoice } from '@renderer/store/settings' -import { Button, Form, Input, message, Select, Space, Switch, Tag, Tabs } from 'antd' +import { Button, Form, Input, message, Select, Space, Switch, Tabs, Tag } from 'antd' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -32,7 +32,6 @@ import { SettingRowTitle, SettingTitle } from '..' - import ASRSettings from './ASRSettings' const CustomVoiceInput = styled.div` @@ -462,7 +461,9 @@ const TTSSettings: FC = () => { console.log('强制刷新TTS服务类型:', currentType) dispatch(setTtsServiceType(currentType)) window.message.success({ - content: t('settings.tts.service_type.refreshed', { defaultValue: '已刷新TTS服务类型设置' }), + content: t('settings.tts.service_type.refreshed', { + defaultValue: '已刷新TTS服务类型设置' + }), key: 'tts-refresh' }) }} @@ -528,7 +529,9 @@ const TTSSettings: FC = () => { title={t('settings.tts.edge_voice.refresh')} /> - {availableVoices.length === 0 && {t('settings.tts.edge_voice.loading')}} + {availableVoices.length === 0 && ( + {t('settings.tts.edge_voice.loading')} + )} )} diff --git a/src/renderer/src/services/ASRService.ts b/src/renderer/src/services/ASRService.ts index 260725fdf6..252ecd9b1d 100644 --- a/src/renderer/src/services/ASRService.ts +++ b/src/renderer/src/services/ASRService.ts @@ -153,7 +153,10 @@ class ASRService { } } else if (data.type === 'error') { console.error('[ASRService] 收到错误消息:', data.message || data.data) - window.message.error({ content: `语音识别错误: ${data.message || data.data?.error || '未知错误'}`, key: 'asr-error' }) + window.message.error({ + content: `语音识别错误: ${data.message || data.data?.error || '未知错误'}`, + key: 'asr-error' + }) } } catch (error) { console.error('[ASRService] 解析WebSocket消息失败:', error, event.data) @@ -175,7 +178,9 @@ class ASRService { } const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), 30000) - console.log(`[ASRService] 将在 ${delay}ms 后尝试重连 (尝试 ${this.reconnectAttempt + 1}/${this.maxReconnectAttempts})`) + console.log( + `[ASRService] 将在 ${delay}ms 后尝试重连 (尝试 ${this.reconnectAttempt + 1}/${this.maxReconnectAttempts})` + ) this.reconnectTimeout = setTimeout(() => { this.reconnectAttempt++ @@ -222,7 +227,7 @@ class ASRService { }) // 等待一秒 - await new Promise(resolve => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 1000)) waitAttempts++ } @@ -355,7 +360,7 @@ class ASRService { // 停止所有轨道 if (this.stream) { - this.stream.getTracks().forEach(track => track.stop()) + this.stream.getTracks().forEach((track) => track.stop()) this.stream = null } @@ -391,7 +396,7 @@ class ASRService { this.isRecording = false this.mediaRecorder = null if (this.stream) { - this.stream.getTracks().forEach(track => track.stop()) + this.stream.getTracks().forEach((track) => track.stop()) this.stream = null } } @@ -420,7 +425,7 @@ class ASRService { const response = await fetch(asrApiUrl, { method: 'POST', headers: { - 'Authorization': `Bearer ${asrApiKey}` + Authorization: `Bearer ${asrApiKey}` }, body: formData }) @@ -510,7 +515,7 @@ class ASRService { // 停止所有轨道 if (this.stream) { - this.stream.getTracks().forEach(track => track.stop()) + this.stream.getTracks().forEach((track) => track.stop()) this.stream = null } diff --git a/src/renderer/src/services/TTSService.ts b/src/renderer/src/services/TTSService.ts index 52d101fd0d..29e0914f2b 100644 --- a/src/renderer/src/services/TTSService.ts +++ b/src/renderer/src/services/TTSService.ts @@ -15,7 +15,8 @@ class TTSService { */ speak = async (text: string): Promise => { try { - const { ttsEnabled, ttsServiceType, ttsApiKey, ttsApiUrl, ttsVoice, ttsModel, ttsEdgeVoice } = store.getState().settings + const { ttsEnabled, ttsServiceType, ttsApiKey, ttsApiUrl, ttsVoice, ttsModel, ttsEdgeVoice } = + store.getState().settings if (!ttsEnabled) { window.message.error({ content: i18n.t('settings.tts.error.not_enabled'), key: 'tts-error' }) @@ -26,7 +27,10 @@ class TTSService { this.stop() // 显示加载提示 - window.message.loading({ content: i18n.t('settings.tts.processing', { defaultValue: '正在生成语音...' }), key: 'tts-loading' }) + window.message.loading({ + content: i18n.t('settings.tts.processing', { defaultValue: '正在生成语音...' }), + key: 'tts-loading' + }) // 初始化为空的Blob,防止类型错误 let audioBlob: Blob = new Blob([], { type: 'audio/wav' }) @@ -125,7 +129,7 @@ class TTSService { const utterance = new SpeechSynthesisUtterance(text) // 获取可用的语音合成声音 - let voices = window.speechSynthesis.getVoices() + const voices = window.speechSynthesis.getVoices() console.log('初始可用的语音合成声音:', voices) // 如果没有可用的声音,等待声音加载 @@ -204,9 +208,8 @@ class TTSService { // 遍历映射表中的候选音色 for (const candidateVoice of voiceMapping[ttsEdgeVoice]) { // 尝试找到匹配的音色 - const matchedVoice = updatedVoices.find(voice => - voice.name.includes(candidateVoice) || - voice.voiceURI.includes(candidateVoice) + const matchedVoice = updatedVoices.find( + (voice) => voice.name.includes(candidateVoice) || voice.voiceURI.includes(candidateVoice) ) if (matchedVoice) { @@ -219,7 +222,7 @@ class TTSService { // 如果映射表没有找到匹配,尝试精确匹配名称 if (!selectedVoice) { - selectedVoice = updatedVoices.find(voice => voice.name === ttsEdgeVoice) + selectedVoice = updatedVoices.find((voice) => voice.name === ttsEdgeVoice) if (selectedVoice) { console.log('找到精确匹配的语音:', selectedVoice.name) } @@ -234,15 +237,16 @@ class TTSService { console.log('检测到Neural音色值,提取语言代码:', langCode) // 先尝试匹配包含语言代码的语音 - selectedVoice = updatedVoices.find(voice => - voice.lang.startsWith(langCode) && - (voice.name.includes(langParts[2]) || // 匹配人名部分,如Xiaoxiao - voice.name.toLowerCase().includes(langParts[2].toLowerCase())) + selectedVoice = updatedVoices.find( + (voice) => + voice.lang.startsWith(langCode) && + (voice.name.includes(langParts[2]) || // 匹配人名部分,如Xiaoxiao + voice.name.toLowerCase().includes(langParts[2].toLowerCase())) ) // 如果没有找到,就匹配该语言的任何语音 if (!selectedVoice) { - selectedVoice = updatedVoices.find(voice => voice.lang.startsWith(langCode)) + selectedVoice = updatedVoices.find((voice) => voice.lang.startsWith(langCode)) if (selectedVoice) { console.log('找到匹配语言的语音:', selectedVoice.name) } @@ -255,9 +259,10 @@ class TTSService { console.log('尝试模糊匹配语音:', ttsEdgeVoice) // 尝试匹配名称中包含的部分 - selectedVoice = updatedVoices.find(voice => - voice.name.toLowerCase().includes(ttsEdgeVoice.toLowerCase()) || - ttsEdgeVoice.toLowerCase().includes(voice.name.toLowerCase()) + selectedVoice = updatedVoices.find( + (voice) => + voice.name.toLowerCase().includes(ttsEdgeVoice.toLowerCase()) || + ttsEdgeVoice.toLowerCase().includes(voice.name.toLowerCase()) ) if (selectedVoice) { @@ -282,7 +287,7 @@ class TTSService { if (langCode) { console.log('尝试根据语言代码匹配语音:', langCode) - selectedVoice = updatedVoices.find(voice => voice.lang.startsWith(langCode)) + selectedVoice = updatedVoices.find((voice) => voice.lang.startsWith(langCode)) if (selectedVoice) { console.log('找到匹配语言代码的语音:', selectedVoice.name) @@ -293,7 +298,7 @@ class TTSService { // 如果还是没有找到,使用默认语音或第一个可用的语音 if (!selectedVoice) { // 先尝试使用默认语音 - selectedVoice = updatedVoices.find(voice => voice.default) + selectedVoice = updatedVoices.find((voice) => voice.default) // 如果没有默认语音,使用第一个可用的语音 if (!selectedVoice && updatedVoices.length > 0) { @@ -310,7 +315,7 @@ class TTSService { } // 设置语音合成参数 - utterance.rate = 1.0 // 语速(0.1-10) + utterance.rate = 1.0 // 语速(0.1-10) utterance.pitch = 1.0 // 音调(0-2) utterance.volume = 1.0 // 音量(0-1) @@ -332,7 +337,7 @@ class TTSService { console.log('文本过长,分段处理以确保完整播放') // 将文本按句子分段 - const sentences = text.split(/[.!?\u3002\uff01\uff1f]/).filter(s => s.trim().length > 0) + const sentences = text.split(/[.!?\u3002\uff01\uff1f]/).filter((s) => s.trim().length > 0) console.log(`将文本分为 ${sentences.length} 个句子进行播放`) // 创建多个语音合成器实例 @@ -359,33 +364,67 @@ class TTSService { // 创建一个有效的音频文件作为占位符 // 这是一个最小的有效WAV文件头 const wavHeader = new Uint8Array([ - 0x52, 0x49, 0x46, 0x46, // "RIFF" - 0x24, 0x00, 0x00, 0x00, // 文件大小 - 0x57, 0x41, 0x56, 0x45, // "WAVE" - 0x66, 0x6d, 0x74, 0x20, // "fmt " - 0x10, 0x00, 0x00, 0x00, // fmt块大小 - 0x01, 0x00, // 格式类型 - 0x01, 0x00, // 通道数 - 0x44, 0xac, 0x00, 0x00, // 采样率 - 0x88, 0x58, 0x01, 0x00, // 字节率 - 0x02, 0x00, // 块对齐 - 0x10, 0x00, // 位深度 - 0x64, 0x61, 0x74, 0x61, // "data" - 0x10, 0x00, 0x00, 0x00 // 数据大小 (16 bytes) - ]); + 0x52, + 0x49, + 0x46, + 0x46, // "RIFF" + 0x24, + 0x00, + 0x00, + 0x00, // 文件大小 + 0x57, + 0x41, + 0x56, + 0x45, // "WAVE" + 0x66, + 0x6d, + 0x74, + 0x20, // "fmt " + 0x10, + 0x00, + 0x00, + 0x00, // fmt块大小 + 0x01, + 0x00, // 格式类型 + 0x01, + 0x00, // 通道数 + 0x44, + 0xac, + 0x00, + 0x00, // 采样率 + 0x88, + 0x58, + 0x01, + 0x00, // 字节率 + 0x02, + 0x00, // 块对齐 + 0x10, + 0x00, // 位深度 + 0x64, + 0x61, + 0x74, + 0x61, // "data" + 0x10, + 0x00, + 0x00, + 0x00 // 数据大小 (16 bytes) + ]) // 添加一些样本数据 - const dummyAudio = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); - const combinedArray = new Uint8Array(wavHeader.length + dummyAudio.length); - combinedArray.set(wavHeader); - combinedArray.set(dummyAudio, wavHeader.length); + const dummyAudio = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + const combinedArray = new Uint8Array(wavHeader.length + dummyAudio.length) + combinedArray.set(wavHeader) + combinedArray.set(dummyAudio, wavHeader.length) // 创建一个有效的WAV文件 let localAudioBlob = new Blob([combinedArray], { type: 'audio/wav' }) console.log('创建了有效WAV文件,大小:', localAudioBlob.size, 'bytes') // 显示成功消息 - window.message.success({ content: i18n.t('settings.tts.playing', { defaultValue: '语音播放中...' }), key: 'tts-loading' }) + window.message.success({ + content: i18n.t('settings.tts.playing', { defaultValue: '语音播放中...' }), + key: 'tts-loading' + }) // 在Edge TTS模式下,我们不需要播放音频元素,因为浏览器已经在播放语音 // 我们只需要创建一个有效的音频Blob作为占位符 @@ -459,26 +498,57 @@ class TTSService { // 创建一个有效的音频数据 // 这是一个最小的有效WAV文件头 const wavHeader = new Uint8Array([ - 0x52, 0x49, 0x46, 0x46, // "RIFF" - 0x24, 0x00, 0x00, 0x00, // 文件大小 - 0x57, 0x41, 0x56, 0x45, // "WAVE" - 0x66, 0x6d, 0x74, 0x20, // "fmt " - 0x10, 0x00, 0x00, 0x00, // fmt块大小 - 0x01, 0x00, // 格式类型 - 0x01, 0x00, // 通道数 - 0x44, 0xac, 0x00, 0x00, // 采样率 - 0x88, 0x58, 0x01, 0x00, // 字节率 - 0x02, 0x00, // 块对齐 - 0x10, 0x00, // 位深度 - 0x64, 0x61, 0x74, 0x61, // "data" - 0x00, 0x00, 0x00, 0x00 // 数据大小 - ]); + 0x52, + 0x49, + 0x46, + 0x46, // "RIFF" + 0x24, + 0x00, + 0x00, + 0x00, // 文件大小 + 0x57, + 0x41, + 0x56, + 0x45, // "WAVE" + 0x66, + 0x6d, + 0x74, + 0x20, // "fmt " + 0x10, + 0x00, + 0x00, + 0x00, // fmt块大小 + 0x01, + 0x00, // 格式类型 + 0x01, + 0x00, // 通道数 + 0x44, + 0xac, + 0x00, + 0x00, // 采样率 + 0x88, + 0x58, + 0x01, + 0x00, // 字节率 + 0x02, + 0x00, // 块对齐 + 0x10, + 0x00, // 位深度 + 0x64, + 0x61, + 0x74, + 0x61, // "data" + 0x00, + 0x00, + 0x00, + 0x00 // 数据大小 + ]) // 添加一些样本数据 - const dummyAudio = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); - const combinedArray = new Uint8Array(wavHeader.length + dummyAudio.length); - combinedArray.set(wavHeader); - combinedArray.set(dummyAudio, wavHeader.length); + const dummyAudio = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + const combinedArray = new Uint8Array(wavHeader.length + dummyAudio.length) + combinedArray.set(wavHeader) + combinedArray.set(dummyAudio, wavHeader.length) localAudioBlob = new Blob([combinedArray], { type: 'audio/wav' }) console.log('创建了有效WAV文件,大小:', localAudioBlob.size, 'bytes') @@ -527,12 +597,12 @@ class TTSService { mediaRecorder.start() // 录制500毫秒 - await new Promise(resolve => setTimeout(resolve, 500)) + await new Promise((resolve) => setTimeout(resolve, 500)) mediaRecorder.stop() // 等待录制完成 - await new Promise(resolve => { + await new Promise((resolve) => { mediaRecorder.onstop = () => { fallbackAudioBlob = new Blob(fallbackAudioChunks, { type: 'audio/wav' }) oscillator.stop() @@ -611,26 +681,57 @@ class TTSService { // 创建一个有效的音频文件作为占位符 // 这是一个最小的有效WAV文件头 const wavHeader = new Uint8Array([ - 0x52, 0x49, 0x46, 0x46, // "RIFF" - 0x24, 0x00, 0x00, 0x00, // 文件大小 - 0x57, 0x41, 0x56, 0x45, // "WAVE" - 0x66, 0x6d, 0x74, 0x20, // "fmt " - 0x10, 0x00, 0x00, 0x00, // fmt块大小 - 0x01, 0x00, // 格式类型 - 0x01, 0x00, // 通道数 - 0x44, 0xac, 0x00, 0x00, // 采样率 - 0x88, 0x58, 0x01, 0x00, // 字节率 - 0x02, 0x00, // 块对齐 - 0x10, 0x00, // 位深度 - 0x64, 0x61, 0x74, 0x61, // "data" - 0x10, 0x00, 0x00, 0x00 // 数据大小 (16 bytes) - ]); + 0x52, + 0x49, + 0x46, + 0x46, // "RIFF" + 0x24, + 0x00, + 0x00, + 0x00, // 文件大小 + 0x57, + 0x41, + 0x56, + 0x45, // "WAVE" + 0x66, + 0x6d, + 0x74, + 0x20, // "fmt " + 0x10, + 0x00, + 0x00, + 0x00, // fmt块大小 + 0x01, + 0x00, // 格式类型 + 0x01, + 0x00, // 通道数 + 0x44, + 0xac, + 0x00, + 0x00, // 采样率 + 0x88, + 0x58, + 0x01, + 0x00, // 字节率 + 0x02, + 0x00, // 块对齐 + 0x10, + 0x00, // 位深度 + 0x64, + 0x61, + 0x74, + 0x61, // "data" + 0x10, + 0x00, + 0x00, + 0x00 // 数据大小 (16 bytes) + ]) // 添加一些样本数据 - const dummyAudio = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); - const combinedArray = new Uint8Array(wavHeader.length + dummyAudio.length); - combinedArray.set(wavHeader); - combinedArray.set(dummyAudio, wavHeader.length); + const dummyAudio = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + const combinedArray = new Uint8Array(wavHeader.length + dummyAudio.length) + combinedArray.set(wavHeader) + combinedArray.set(dummyAudio, wavHeader.length) audioBlob = new Blob([combinedArray], { type: 'audio/wav' }) console.log('创建了有效WAV文件,大小:', audioBlob.size, 'bytes') @@ -689,17 +790,20 @@ class TTSService { */ private cleanTextForSpeech(text: string): string { // 获取最新的TTS设置 - const { ttsFilterOptions = { - filterThinkingProcess: true, - filterMarkdown: true, - filterCodeBlocks: true, - filterHtmlTags: true, - maxTextLength: 4000 - }, ttsServiceType } = store.getState().settings; + const { + ttsFilterOptions = { + filterThinkingProcess: true, + filterMarkdown: true, + filterCodeBlocks: true, + filterHtmlTags: true, + maxTextLength: 4000 + }, + ttsServiceType + } = store.getState().settings // 输出当前的TTS服务类型,便于调试 console.log('清理文本时使用的TTS服务类型:', ttsServiceType || 'openai') - let cleanedText = text; + let cleanedText = text // 根据过滤选项进行处理 @@ -708,23 +812,23 @@ class TTSService { cleanedText = cleanedText // 移除加粗和斜体标记 .replace(/\*\*([^*]+)\*\*/g, '$1') // **bold** -> bold - .replace(/\*([^*]+)\*/g, '$1') // *italic* -> italic - .replace(/__([^_]+)__/g, '$1') // __bold__ -> bold - .replace(/_([^_]+)_/g, '$1') // _italic_ -> italic + .replace(/\*([^*]+)\*/g, '$1') // *italic* -> italic + .replace(/__([^_]+)__/g, '$1') // __bold__ -> bold + .replace(/_([^_]+)_/g, '$1') // _italic_ -> italic // 移除链接格式,只保留链接文本 - .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // [text](url) -> text + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // [text](url) -> text } // 移除代码块 if (ttsFilterOptions.filterCodeBlocks) { cleanedText = cleanedText - .replace(/```[\s\S]*?```/g, '') // 移除代码块 - .replace(/`([^`]+)`/g, '$1'); // `code` -> code + .replace(/```[\s\S]*?```/g, '') // 移除代码块 + .replace(/`([^`]+)`/g, '$1') // `code` -> code } // 移除HTML标签 if (ttsFilterOptions.filterHtmlTags) { - cleanedText = cleanedText.replace(/<[^>]*>/g, ''); + cleanedText = cleanedText.replace(/<[^>]*>/g, '') } // 基本清理(始终执行) @@ -734,9 +838,9 @@ class TTSService { // 将多个连续的换行替换为单个换行 .replace(/\n+/g, '\n') // 移除行首和行尾的空白字符 - .trim(); + .trim() - return cleanedText; + return cleanedText } /** @@ -746,95 +850,99 @@ class TTSService { */ private removeThinkingProcess(text: string): string { // 获取最新的TTS设置 - const { ttsFilterOptions = { - filterThinkingProcess: true, - filterMarkdown: true, - filterCodeBlocks: true, - filterHtmlTags: true, - maxTextLength: 4000 - }, ttsServiceType } = store.getState().settings; + const { + ttsFilterOptions = { + filterThinkingProcess: true, + filterMarkdown: true, + filterCodeBlocks: true, + filterHtmlTags: true, + maxTextLength: 4000 + }, + ttsServiceType + } = store.getState().settings // 输出当前的TTS服务类型,便于调试 console.log('移除思考过程时使用的TTS服务类型:', ttsServiceType || 'openai') // 如果不需要过滤思考过程,直接返回原文本 if (!ttsFilterOptions.filterThinkingProcess) { - return text; + return text } // 如果整个文本都是{'text': '...'}格式,则不处理 // 这种情况可能是伪思考过程,实际上是整个回答 - const isFullTextJson = text.trim().startsWith('{') && - text.includes('"text":') && - text.trim().endsWith('}') && - !text.includes('\n\n'); + const isFullTextJson = + text.trim().startsWith('{') && text.includes('"text":') && text.trim().endsWith('}') && !text.includes('\n\n') // 如果文本中包含多个段落或明显的思考过程标记,则处理 - const hasThinkingMarkers = text.includes('') || - text.includes('') || - text.includes('[THINKING]') || - text.includes('```thinking'); + const hasThinkingMarkers = + text.includes('') || + text.includes('') || + text.includes('[THINKING]') || + text.includes('```thinking') // 如果文本以JSON格式开头,且不是整个文本都是JSON,或者包含思考过程标记 if ((text.trim().startsWith('{') && text.includes('"text":') && !isFullTextJson) || hasThinkingMarkers) { // 尝试提取JSON中的text字段 try { - const match = text.match(/"text":\s*"([^"]+)"/); + const match = text.match(/"text":\s*"([^"]+)"/) if (match && match[1]) { // 只返回text字段的内容 - return match[1].replace(/\\n/g, '\n').replace(/\\"/g, '"'); + return match[1].replace(/\\n/g, '\n').replace(/\\"/g, '"') } } catch (e) { - console.error('解析JSON失败:', e); + console.error('解析JSON失败:', e) } } // 直接检查是否以开头 - const trimmedText = text.trim(); - console.log('检查是否以开头:', trimmedText.startsWith('')); + const trimmedText = text.trim() + console.log('检查是否以开头:', trimmedText.startsWith('')) if (trimmedText.startsWith('')) { // 如果文本以开头,则尝试找到对应的结尾标签 - const endTagIndex = text.indexOf(''); - console.log('结束标签位置:', endTagIndex); + const endTagIndex = text.indexOf('') + console.log('结束标签位置:', endTagIndex) if (endTagIndex !== -1) { // 找到结束标签,去除...部分 - const thinkContent = text.substring(0, endTagIndex + 9); // 思考过程部分 - const afterThinkTag = text.substring(endTagIndex + 9).trim(); // 9是的长度 + const thinkContent = text.substring(0, endTagIndex + 9) // 思考过程部分 + const afterThinkTag = text.substring(endTagIndex + 9).trim() // 9是的长度 - console.log('思考过程内容长度:', thinkContent.length); - console.log('思考过程后的内容长度:', afterThinkTag.length); - console.log('思考过程后的内容开头:', afterThinkTag.substring(0, 50)); + console.log('思考过程内容长度:', thinkContent.length) + console.log('思考过程后的内容长度:', afterThinkTag.length) + console.log('思考过程后的内容开头:', afterThinkTag.substring(0, 50)) if (afterThinkTag) { - console.log('找到标签,已移除思考过程'); - return afterThinkTag; + console.log('找到标签,已移除思考过程') + return afterThinkTag } else { // 如果思考过程后没有内容,则尝试提取思考过程中的有用信息 - console.log('思考过程后没有内容,尝试提取思考过程中的有用信息'); + console.log('思考过程后没有内容,尝试提取思考过程中的有用信息') // 提取之间的内容 - const thinkContentText = text.substring(text.indexOf('') + 7, endTagIndex).trim(); + const thinkContentText = text.substring(text.indexOf('') + 7, endTagIndex).trim() // 如果思考过程中包含“这是”或“This is”等关键词,可能是有用的信息 - if (thinkContentText.includes('这是') || - thinkContentText.includes('This is') || - thinkContentText.includes('The error') || - thinkContentText.includes('错误')) { - + if ( + thinkContentText.includes('这是') || + thinkContentText.includes('This is') || + thinkContentText.includes('The error') || + thinkContentText.includes('错误') + ) { // 尝试找到最后一个段落,可能包含总结信息 - const paragraphs = thinkContentText.split(/\n\s*\n/); + const paragraphs = thinkContentText.split(/\n\s*\n/) if (paragraphs.length > 0) { - const lastParagraph = paragraphs[paragraphs.length - 1].trim(); - if (lastParagraph.length > 50) { // 确保段落足够长 - console.log('从思考过程中提取了最后一个段落'); - return lastParagraph; + const lastParagraph = paragraphs[paragraphs.length - 1].trim() + if (lastParagraph.length > 50) { + // 确保段落足够长 + console.log('从思考过程中提取了最后一个段落') + return lastParagraph } } // 如果没有找到合适的段落,返回整个思考过程 - console.log('返回整个思考过程内容'); - return thinkContentText; + console.log('返回整个思考过程内容') + return thinkContentText } } } @@ -842,35 +950,35 @@ class TTSService { // 先处理标签 if (text.includes('')) { - const startIndex = text.indexOf(''); - const endIndex = text.indexOf(''); + const startIndex = text.indexOf('') + const endIndex = text.indexOf('') if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) { - console.log('找到标签,起始位置:', startIndex, '结束位置:', endIndex); + console.log('找到标签,起始位置:', startIndex, '结束位置:', endIndex) // 提取之间的内容 - const thinkContent = text.substring(startIndex + 7, endIndex); + const thinkContent = text.substring(startIndex + 7, endIndex) // 提取后面的内容 - const afterThinkContent = text.substring(endIndex + 9).trim(); // 9是的长度 + const afterThinkContent = text.substring(endIndex + 9).trim() // 9是的长度 - console.log('内容长度:', thinkContent.length); - console.log('后内容长度:', afterThinkContent.length); + console.log('内容长度:', thinkContent.length) + console.log('后内容长度:', afterThinkContent.length) if (afterThinkContent) { // 如果后面有内容,则使用该内容 - console.log('使用后面的内容'); - return afterThinkContent; + console.log('使用后面的内容') + return afterThinkContent } else { // 如果后面没有内容,则使用思考过程中的内容 - console.log('使用标签中的内容'); - return thinkContent; + console.log('使用标签中的内容') + return thinkContent } } } // 如果没有标签或处理失败,则移除其他思考过程标记 - let processedText = text + const processedText = text // 移除HTML标记的思考过程 .replace(/[\s\S]*?<\/thinking>/gi, '') .replace(/[\s\S]*?<\/think>/gi, '') @@ -881,31 +989,38 @@ class TTSService { .replace(/```thinking[\s\S]*?```/gi, '') .replace(/```think[\s\S]*?```/gi, '') // 移除开头的“我先思考一下”类似的句子 - .replace(/^(\s*)(\S+\s+)?(\S+\s+)?(\S+\s+)?(我|让我|让我们|我们|我先|我来)(思考|分析|理解|看一下|想一想)[^\n]*\n/i, '') + .replace( + /^(\s*)(\S+\s+)?(\S+\s+)?(\S+\s+)?(我|让我|让我们|我们|我先|我来)(思考|分析|理解|看一下|想一想)[^\n]*\n/i, + '' + ) // 移除开头的“Let me think”类似的句子 - .replace(/^(\s*)(\S+\s+)?(\S+\s+)?(\S+\s+)?(Let me|I'll|I will|I need to|Let's|I'm going to)\s+(think|analyze|understand|consider|break down)[^\n]*\n/i, '') + .replace( + /^(\s*)(\S+\s+)?(\S+\s+)?(\S+\s+)?(Let me|I'll|I will|I need to|Let's|I'm going to)\s+(think|analyze|understand|consider|break down)[^\n]*\n/i, + '' + ) // 移除开头的“To answer this question”类似的句子 - .replace(/^(\s*)(\S+\s+)?(\S+\s+)?(\S+\s+)?(To answer this|To solve this|To address this|To respond to this)[^\n]*\n/i, '') + .replace( + /^(\s*)(\S+\s+)?(\S+\s+)?(\S+\s+)?(To answer this|To solve this|To address this|To respond to this)[^\n]*\n/i, + '' + ) // 如果文本中包含“我的回答是”或“我的答案是”,只保留这之后的内容 const answerMarkers = [ /[\n\r]+(\s*)(我的|最终|最终的|正确的|完整的)?(回答|答案|结论|解决方案)(是|如下|就是|就是如下)[\s::]*/i, /[\n\r]+(\s*)(My|The|Final|Complete|Correct)\s+(answer|response|solution|conclusion)\s+(is|would be|follows)[\s:]*/i - ]; + ] for (const marker of answerMarkers) { - const parts = processedText.split(marker); + const parts = processedText.split(marker) if (parts.length > 1) { // 取最后一个匹配后的内容 - return parts[parts.length - 1].trim(); + return parts[parts.length - 1].trim() } } - return processedText; + return processedText } - - /** * 从消息中提取文本并转换为语音 * @param message 消息对象 @@ -923,25 +1038,25 @@ class TTSService { console.log('原始文本开头:', text.substring(0, 100)) // 先移除思考过程 - const processedText = this.removeThinkingProcess(text); - console.log('移除思考过程后文本长度:', processedText.length); - console.log('处理后文本开头:', processedText.substring(0, 100)); - text = processedText; + const processedText = this.removeThinkingProcess(text) + console.log('移除思考过程后文本长度:', processedText.length) + console.log('处理后文本开头:', processedText.substring(0, 100)) + text = processedText // 清理文本,移除不需要的标点符号 text = this.cleanTextForSpeech(text) console.log('清理标点符号后文本长度:', text.length) // 获取最新的TTS设置 - const latestSettings = store.getState().settings; + const latestSettings = store.getState().settings const ttsFilterOptions = latestSettings.ttsFilterOptions || { filterThinkingProcess: true, filterMarkdown: true, filterCodeBlocks: true, filterHtmlTags: true, maxTextLength: 4000 - }; - const ttsServiceType = latestSettings.ttsServiceType; + } + const ttsServiceType = latestSettings.ttsServiceType // 输出当前的TTS服务类型,便于调试 console.log('当前消息播放使用的TTS服务类型:', ttsServiceType || 'openai') @@ -956,7 +1071,7 @@ class TTSService { // 如果消息过长,可能会导致TTS API超时或失败 // 根据设置的最大文本长度进行截断 - const maxLength = ttsFilterOptions.maxTextLength || 4000; // 默认为4000 + const maxLength = ttsFilterOptions.maxTextLength || 4000 // 默认为4000 if (text.length > maxLength) { text = text.substring(0, maxLength) + '...' console.log(`文本过长,已截断为${maxLength}个字符`) @@ -1006,7 +1121,7 @@ class TTSService { const fadeOut = () => { if (currentStep < fadeOutSteps && this.audio) { - this.audio.volume = Math.max(0, originalVolume - (fadeStep * currentStep)) + this.audio.volume = Math.max(0, originalVolume - fadeStep * currentStep) currentStep++ setTimeout(fadeOut, fadeOutInterval) } else {