From 21e195c51ab795a8c2e3b3452096476887f7ccdf Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:54:35 +0800 Subject: [PATCH] Update ASRServerService.ts --- src/renderer/src/services/ASRServerService.ts | 838 +++--------------- 1 file changed, 107 insertions(+), 731 deletions(-) diff --git a/src/renderer/src/services/ASRServerService.ts b/src/renderer/src/services/ASRServerService.ts index 3e105bcf4f..506715a4e8 100644 --- a/src/renderer/src/services/ASRServerService.ts +++ b/src/renderer/src/services/ASRServerService.ts @@ -1,753 +1,129 @@ -import { AudioOutlined, PlusOutlined, ReloadOutlined, SoundOutlined } from '@ant-design/icons' -import { useTheme } from '@renderer/context/ThemeProvider' -import TTSService from '@renderer/services/TTSService' -import store, { useAppDispatch } from '@renderer/store' -import { - addTtsCustomModel, - addTtsCustomVoice, - removeTtsCustomModel, - removeTtsCustomVoice, - resetTtsCustomValues, - setTtsApiKey, - setTtsApiUrl, - setTtsEdgeVoice, - setTtsEnabled, - setTtsFilterOptions, - setTtsModel, - setTtsServiceType, - setTtsVoice -} from '@renderer/store/settings' -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' -import styled from 'styled-components' +import i18n from '@renderer/i18n' -import { - SettingContainer, - SettingDivider, - SettingGroup, - SettingHelpText, - SettingRow, - SettingRowTitle, - SettingTitle -} from '..' -import ASRSettings from './ASRSettings' +// 使用window.electron而不是直接导入electron模块 +// 这样可以避免__dirname不可用的问题 -const CustomVoiceInput = styled.div` - display: flex; - flex-direction: column; - gap: 8px; -` +class ASRServerService { + private serverProcess: any = null + private isServerRunning = false -const TagsContainer = styled.div` - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 8px; - margin-bottom: 16px; -` + /** + * 启动ASR服务器 + * @returns Promise 是否成功启动 + */ + startServer = async (): Promise => { + if (this.isServerRunning) { + console.log('[ASRServerService] 服务器已经在运行中') + window.message.info({ content: i18n.t('settings.asr.server.already_running'), key: 'asr-server' }) + return true + } -const EmptyText = styled.div` - color: rgba(0, 0, 0, 0.45); - padding: 4px 0; -` + try { + console.log('[ASRServerService] 正在启动ASR服务器...') + window.message.loading({ content: i18n.t('settings.asr.server.starting'), key: 'asr-server' }) -const InputGroup = styled.div` - display: flex; - gap: 8px; - margin-bottom: 8px; -` + // 使用IPC调用主进程启动服务器 + const result = await window.api.asrServer.startServer() -const FlexContainer = styled.div` - display: flex; - gap: 8px; -` - -const FilterOptionItem = styled.div` - margin-bottom: 16px; - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; -` - -const LengthLabel = styled.span` - margin-right: 8px; -` - -const LoadingText = styled.div` - margin-top: 8px; - color: #999; -` - -const VoiceSelectContainer = styled.div` - display: flex; - gap: 8px; - margin-bottom: 8px; -` - -const TTSSettings: FC = () => { - const { t } = useTranslation() - const { theme } = useTheme() - const dispatch = useAppDispatch() - - // 从Redux获取TTS设置 - const ttsEnabled = useSelector((state: any) => state.settings.ttsEnabled) - const ttsServiceType = useSelector((state: any) => state.settings.ttsServiceType || 'openai') - const ttsApiKey = useSelector((state: any) => state.settings.ttsApiKey) - const ttsApiUrl = useSelector((state: any) => state.settings.ttsApiUrl) - const ttsVoice = useSelector((state: any) => state.settings.ttsVoice) - const ttsModel = useSelector((state: any) => state.settings.ttsModel) - const ttsEdgeVoice = useSelector((state: any) => state.settings.ttsEdgeVoice || 'zh-CN-XiaoxiaoNeural') - const ttsCustomVoices = useSelector((state: any) => state.settings.ttsCustomVoices || []) - const ttsCustomModels = useSelector((state: any) => state.settings.ttsCustomModels || []) - const ttsFilterOptions = useSelector( - (state: any) => - state.settings.ttsFilterOptions || { - filterThinkingProcess: true, - filterMarkdown: true, - filterCodeBlocks: true, - filterHtmlTags: true, - maxTextLength: 4000 + if (result.success) { + this.isServerRunning = true + this.serverProcess = result.pid + console.log('[ASRServerService] ASR服务器启动成功,PID:', result.pid) + window.message.success({ content: i18n.t('settings.asr.server.started'), key: 'asr-server' }) + return true + } else { + console.error('[ASRServerService] ASR服务器启动失败:', result.error) + window.message.error({ + content: i18n.t('settings.asr.server.start_failed') + ': ' + result.error, + key: 'asr-server' + }) + return false } - ) - - // 新增自定义音色和模型的状态 - const [newVoice, setNewVoice] = useState('') - const [newModel, setNewModel] = useState('') - - // 浏览器可用的语音列表 - const [availableVoices, setAvailableVoices] = useState<{ label: string; value: string }[]>([]) - - // 预定义的浏览器 TTS音色列表 - const predefinedVoices = [ - { label: '小晓 (女声, 中文)', value: 'zh-CN-XiaoxiaoNeural' }, - { label: '云扬 (男声, 中文)', value: 'zh-CN-YunyangNeural' }, - { label: '晓晓 (女声, 中文)', value: 'zh-CN-XiaoxiaoNeural' }, - { label: '晓涵 (女声, 中文)', value: 'zh-CN-XiaohanNeural' }, - { label: '晓诗 (女声, 中文)', value: 'zh-CN-XiaoshuangNeural' }, - { label: '晓瑞 (女声, 中文)', value: 'zh-CN-XiaoruiNeural' }, - { label: '晓墨 (女声, 中文)', value: 'zh-CN-XiaomoNeural' }, - { label: '晓然 (男声, 中文)', value: 'zh-CN-XiaoranNeural' }, - { label: '晓坤 (男声, 中文)', value: 'zh-CN-XiaokunNeural' }, - { label: 'Aria (Female, English)', value: 'en-US-AriaNeural' }, - { label: 'Guy (Male, English)', value: 'en-US-GuyNeural' }, - { label: 'Jenny (Female, English)', value: 'en-US-JennyNeural' }, - { label: 'Ana (Female, Spanish)', value: 'es-ES-ElviraNeural' }, - { label: 'Ichiro (Male, Japanese)', value: 'ja-JP-KeitaNeural' }, - { label: 'Nanami (Female, Japanese)', value: 'ja-JP-NanamiNeural' }, - // 添加更多常用的语音 - { label: 'Microsoft David (en-US)', value: 'Microsoft David Desktop - English (United States)' }, - { label: 'Microsoft Zira (en-US)', value: 'Microsoft Zira Desktop - English (United States)' }, - { label: 'Microsoft Mark (en-US)', value: 'Microsoft Mark Online (Natural) - English (United States)' }, - { label: 'Microsoft Aria (en-US)', value: 'Microsoft Aria Online (Natural) - English (United States)' }, - { label: 'Google US English', value: 'Google US English' }, - { label: 'Google UK English Female', value: 'Google UK English Female' }, - { label: 'Google UK English Male', value: 'Google UK English Male' }, - { label: 'Google 日本語', value: 'Google 日本語' }, - { label: 'Google 普通话(中国大陆)', value: 'Google 普通话(中国大陆)' }, - { label: 'Google 粤語(香港)', value: 'Google 粤語(香港)' } - ] - - // 获取浏览器可用的语音列表 - const getVoices = () => { - if (typeof window !== 'undefined' && 'speechSynthesis' in window) { - // 先触发一下语音合成引擎,确保它已经初始化 - window.speechSynthesis.cancel() - - // 获取浏览器可用的语音列表 - const voices = window.speechSynthesis.getVoices() - console.log('获取到的语音列表:', voices) - console.log('语音列表长度:', voices.length) - - // 转换浏览器语音列表为选项格式 - const browserVoices = voices.map((voice) => ({ - label: `${voice.name} (${voice.lang})${voice.default ? ' - 默认' : ''}`, - value: voice.name, - lang: voice.lang, - isNative: true // 标记为浏览器原生语音 - })) - - // 添加语言信息到预定义语音 - const enhancedPredefinedVoices = predefinedVoices.map((voice) => ({ - ...voice, - lang: voice.value.split('-').slice(0, 2).join('-'), - isNative: false // 标记为非浏览器原生语音 - })) - - // 合并所有语音列表 - let allVoices = [...browserVoices] - - // 如果浏览器语音少于5个,添加预定义语音 - if (browserVoices.length < 5) { - allVoices = [...browserVoices, ...enhancedPredefinedVoices] - } - - // 去除重复项,优先保留浏览器原生语音 - const uniqueVoices = allVoices.filter((voice, index, self) => { - const firstIndex = self.findIndex((v) => v.value === voice.value) - // 如果是原生语音或者是第一次出现,则保留 - return voice.isNative || firstIndex === index + } catch (error) { + console.error('[ASRServerService] 启动ASR服务器时出错:', error) + window.message.error({ + content: i18n.t('settings.asr.server.start_failed') + ': ' + (error as Error).message, + key: 'asr-server' }) + return false + } + } - // 按语言分组并排序 - const groupedVoices = uniqueVoices.sort((a, b) => { - // 先按语言排序 - if (a.lang !== b.lang) { - return a.lang.localeCompare(b.lang) - } - // 同语言下,原生语音优先 - if (a.isNative !== b.isNative) { - return a.isNative ? -1 : 1 - } - // 最后按名称排序 - return a.label.localeCompare(b.label) + /** + * 停止ASR服务器 + * @returns Promise 是否成功停止 + */ + stopServer = async (): Promise => { + if (!this.isServerRunning || !this.serverProcess) { + console.log('[ASRServerService] 服务器未运行') + window.message.info({ content: i18n.t('settings.asr.server.not_running'), key: 'asr-server' }) + return true + } + + try { + console.log('[ASRServerService] 正在停止ASR服务器...') + window.message.loading({ content: i18n.t('settings.asr.server.stopping'), key: 'asr-server' }) + + // 使用IPC调用主进程停止服务器 + const result = await window.api.asrServer.stopServer(this.serverProcess) + + if (result.success) { + this.isServerRunning = false + this.serverProcess = null + console.log('[ASRServerService] ASR服务器已停止') + window.message.success({ content: i18n.t('settings.asr.server.stopped'), key: 'asr-server' }) + return true + } else { + console.error('[ASRServerService] ASR服务器停止失败:', result.error) + window.message.error({ + content: i18n.t('settings.asr.server.stop_failed') + ': ' + result.error, + key: 'asr-server' + }) + return false + } + } catch (error) { + console.error('[ASRServerService] 停止ASR服务器时出错:', error) + window.message.error({ + content: i18n.t('settings.asr.server.stop_failed') + ': ' + (error as Error).message, + key: 'asr-server' }) - - setAvailableVoices(groupedVoices) - console.log('设置可用语音列表:', groupedVoices) - } else { - // 如果浏览器不支持Web Speech API,使用预定义的语音列表 - console.log('浏览器不支持Web Speech API,使用预定义的语音列表') - setAvailableVoices(predefinedVoices) + return false } } - // 刷新语音列表 - const refreshVoices = () => { - console.log('手动刷新语音列表') - message.loading({ - content: t('settings.tts.edge_voice.refreshing', { defaultValue: '正在刷新语音列表...' }), - key: 'refresh-voices' - }) - - // 先清空当前列表 - setAvailableVoices([]) - - // 强制重新加载语音列表 - if (typeof window !== 'undefined' && 'speechSynthesis' in window) { - window.speechSynthesis.cancel() - - // 尝试多次获取语音列表 - setTimeout(() => { - getVoices() - setTimeout(() => { - getVoices() - message.success({ - content: t('settings.tts.edge_voice.refreshed', { defaultValue: '语音列表已刷新' }), - key: 'refresh-voices' - }) - }, 1000) - }, 500) - } else { - // 如果浏览器不支持Web Speech API,使用预定义的语音列表 - setAvailableVoices(predefinedVoices) - message.success({ - content: t('settings.tts.edge_voice.refreshed', { defaultValue: '语音列表已刷新' }), - key: 'refresh-voices' - }) - } + /** + * 检查ASR服务器是否正在运行 + * @returns boolean 是否正在运行 + */ + isRunning = (): boolean => { + return this.isServerRunning } - useEffect(() => { - // 初始化语音合成引擎 - if (typeof window !== 'undefined' && 'speechSynthesis' in window) { - // 触发语音合成引擎初始化 - window.speechSynthesis.cancel() - - // 设置voiceschanged事件处理程序 - const voicesChangedHandler = () => { - console.log('检测到voiceschanged事件,重新获取语音列表') - getVoices() - } - - // 添加事件监听器 - window.speechSynthesis.onvoiceschanged = voicesChangedHandler - - // 立即获取可用的语音 - getVoices() - - // 创建多个定时器,在不同时间点尝试获取语音列表 - // 这是因为不同浏览器加载语音列表的时间不同 - const timers = [ - setTimeout(() => getVoices(), 500), - setTimeout(() => getVoices(), 1000), - setTimeout(() => getVoices(), 2000) - ] - - return () => { - // 清理事件监听器和定时器 - window.speechSynthesis.onvoiceschanged = null - timers.forEach((timer) => clearTimeout(timer)) - } - } else { - // 如果浏览器不支持Web Speech API,使用预定义的语音列表 - setAvailableVoices(predefinedVoices) - return () => {} - } - }, [getVoices, predefinedVoices]) - - // 测试TTS功能 - const testTTS = async () => { - if (!ttsEnabled) { - window.message.error({ content: t('settings.tts.error.not_enabled'), key: 'tts-test' }) - return - } - - // 获取最新的服务类型设置 - const latestSettings = store.getState().settings - const currentServiceType = latestSettings.ttsServiceType || 'openai' - console.log('测试TTS时使用的服务类型:', currentServiceType) - console.log('测试时完整TTS设置:', { - ttsEnabled: latestSettings.ttsEnabled, - ttsServiceType: latestSettings.ttsServiceType, - ttsApiKey: latestSettings.ttsApiKey ? '已设置' : '未设置', - ttsVoice: latestSettings.ttsVoice, - ttsModel: latestSettings.ttsModel, - ttsEdgeVoice: latestSettings.ttsEdgeVoice - }) - - // 根据服务类型检查必要的参数 - if (currentServiceType === 'openai') { - if (!ttsApiKey) { - window.message.error({ content: t('settings.tts.error.no_api_key'), key: 'tts-test' }) - return - } - - if (!ttsVoice) { - window.message.error({ content: t('settings.tts.error.no_voice'), key: 'tts-test' }) - return - } - - if (!ttsModel) { - window.message.error({ content: t('settings.tts.error.no_model'), key: 'tts-test' }) - return - } - } else if (currentServiceType === 'edge') { - if (!ttsEdgeVoice) { - window.message.error({ content: t('settings.tts.error.no_edge_voice'), key: 'tts-test' }) - return - } - } - - await TTSService.speak('这是一段测试语音,用于测试TTS功能是否正常工作。') + /** + * 获取ASR服务器网页URL + * @returns string 网页URL + */ + getServerUrl = (): string => { + return 'http://localhost:8080' } - // 添加自定义音色 - const handleAddVoice = () => { - if (!newVoice) { - window.message.error({ content: '请输入音色', key: 'add-voice' }) - return - } - - // 确保添加的是字符串 - const voiceStr = typeof newVoice === 'string' ? newVoice : String(newVoice) - dispatch(addTtsCustomVoice(voiceStr)) - setNewVoice('') + /** + * 获取ASR服务器文件路径 + * @returns string 服务器文件路径 + */ + getServerFilePath = (): string => { + // 使用相对路径,因为window.electron.app.getAppPath()不可用 + return process.env.NODE_ENV === 'development' + ? 'src/renderer/src/assets/asr-server/server.js' + : 'public/asr-server/server.js' } - // 添加自定义模型 - const handleAddModel = () => { - if (!newModel) { - window.message.error({ content: '请输入模型', key: 'add-model' }) - return - } - - // 确保添加的是字符串 - const modelStr = typeof newModel === 'string' ? newModel : String(newModel) - dispatch(addTtsCustomModel(modelStr)) - setNewModel('') + /** + * 打开ASR服务器网页 + */ + openServerPage = (): void => { + window.open(this.getServerUrl(), '_blank') } - - // 删除自定义音色 - const handleRemoveVoice = (voice: string) => { - // 确保删除的是字符串 - const voiceStr = typeof voice === 'string' ? voice : String(voice) - dispatch(removeTtsCustomVoice(voiceStr)) - } - - // 删除自定义模型 - const handleRemoveModel = (model: string) => { - // 确保删除的是字符串 - const modelStr = typeof model === 'string' ? model : String(model) - dispatch(removeTtsCustomModel(modelStr)) - } - - return ( - - - - - {t('settings.voice.title')} - - - - - {t('settings.tts.tab_title')} - - ), - children: ( -
- - - {t('settings.tts.enable')} - dispatch(setTtsEnabled(checked))} /> - - {t('settings.tts.enable.help')} - - - {/* 重置按钮 */} - - - {t('settings.tts.reset_title')} - - - {t('settings.tts.reset_help')} - - - {t('settings.tts.api_settings')} -
- {/* TTS服务类型选择 */} - - - dispatch(setTtsApiUrl(e.target.value))} - placeholder={t('settings.tts.api_url.placeholder')} - disabled={!ttsEnabled} - /> - - - )} - - {/* 浏览器 TTS设置 */} - {ttsServiceType === 'edge' && ( - - - dispatch(setTtsVoice(value))} - options={ttsCustomVoices.map((voice: any) => { - // 确保voice是字符串 - const voiceStr = typeof voice === 'string' ? voice : String(voice) - return { label: voiceStr, value: voiceStr } - })} - disabled={!ttsEnabled} - style={{ width: '100%' }} - placeholder={t('settings.tts.voice.placeholder')} - showSearch - optionFilterProp="label" - allowClear - /> - - - {/* 自定义音色列表 */} - - {ttsCustomVoices && ttsCustomVoices.length > 0 ? ( - ttsCustomVoices.map((voice: any, index: number) => { - // 确保voice是字符串 - const voiceStr = typeof voice === 'string' ? voice : String(voice) - return ( - handleRemoveVoice(voiceStr)} - style={{ padding: '4px 8px' }}> - {voiceStr} - - ) - }) - ) : ( - {t('settings.tts.voice_empty')} - )} - - - {/* 添加自定义音色 */} - - - setNewVoice(e.target.value)} - disabled={!ttsEnabled} - style={{ flex: 1 }} - /> - - - - - {/* 模型选择 */} - - setNewModel(e.target.value)} - disabled={!ttsEnabled} - style={{ flex: 1 }} - /> - - - - - )} - - {/* TTS过滤选项 */} - - - dispatch(setTtsFilterOptions({ filterThinkingProcess: checked }))} - disabled={!ttsEnabled} - />{' '} - {t('settings.tts.filter.thinking_process')} - - - dispatch(setTtsFilterOptions({ filterMarkdown: checked }))} - disabled={!ttsEnabled} - />{' '} - {t('settings.tts.filter.markdown')} - - - dispatch(setTtsFilterOptions({ filterCodeBlocks: checked }))} - disabled={!ttsEnabled} - />{' '} - {t('settings.tts.filter.code_blocks')} - - - dispatch(setTtsFilterOptions({ filterHtmlTags: checked }))} - disabled={!ttsEnabled} - />{' '} - {t('settings.tts.filter.html_tags')} - - - {t('settings.tts.max_text_length')}: -