From 5d57eb18ea7ba575cc50653e26dfa781698bb0a9 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sat, 12 Apr 2025 13:46:34 +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 --- src/main/index.ts | 5 +- src/renderer/src/components/ASRButton.tsx | 1 + .../components/DraggableVoiceCallWindow.tsx | 81 ++-- src/renderer/src/components/TTSButton.tsx | 86 +++- .../src/components/TTSHighlightedText.tsx | 96 +++++ .../src/components/TTSProgressBar.tsx | 243 +++++++++++ .../src/components/TTSSegmentedText.tsx | 76 ++++ .../src/components/VoiceCallModal.tsx | 5 +- src/renderer/src/hooks/useAppInit.ts | 28 +- src/renderer/src/i18n/locales/en-us.json | 26 +- src/renderer/src/i18n/locales/zh-cn.json | 10 + .../src/pages/home/Messages/Message.tsx | 43 +- .../pages/home/Messages/MessageContent.tsx | 26 +- .../pages/home/Messages/MessageMenubar.tsx | 19 +- .../settings/TTSSettings/ASRSettings.tsx | 25 +- src/renderer/src/services/TTSService.ts | 26 +- src/renderer/src/services/tts/TTSService.ts | 384 +++++++++++++++++- .../src/services/tts/TextSegmenter.ts | 85 ++++ src/renderer/src/store/settings.ts | 6 + 19 files changed, 1185 insertions(+), 86 deletions(-) create mode 100644 src/renderer/src/components/TTSHighlightedText.tsx create mode 100644 src/renderer/src/components/TTSProgressBar.tsx create mode 100644 src/renderer/src/components/TTSSegmentedText.tsx create mode 100644 src/renderer/src/services/tts/TextSegmenter.ts diff --git a/src/main/index.ts b/src/main/index.ts index 1ca7d4a91e..71a454048a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -8,7 +8,6 @@ import Logger from 'electron-log' import { registerIpc } from './ipc' import { configManager } from './services/ConfigManager' import mcpService from './services/MCPService' -import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler' import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient' import { registerShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' @@ -49,8 +48,8 @@ if (!app.requestSingleInstanceLock()) { registerIpc(mainWindow, app) - // 注册MsTTS IPC处理程序 - registerMsTTSIpcHandlers() + // 注意: MsTTS IPC处理程序已在ipc.ts中注册 + // 不需要再次调用registerMsTTSIpcHandlers() replaceDevtoolsFont(mainWindow) diff --git a/src/renderer/src/components/ASRButton.tsx b/src/renderer/src/components/ASRButton.tsx index ccfb05c770..11bbaa2bcc 100644 --- a/src/renderer/src/components/ASRButton.tsx +++ b/src/renderer/src/components/ASRButton.tsx @@ -210,6 +210,7 @@ const StyledButton = styled(Button)` justify-content: center; align-items: center; padding: 0; + border: none; /* 移除边框 */ &.anticon, &.iconfont { transition: all 0.3s ease; diff --git a/src/renderer/src/components/DraggableVoiceCallWindow.tsx b/src/renderer/src/components/DraggableVoiceCallWindow.tsx index 877f8698de..49d3b932b9 100644 --- a/src/renderer/src/components/DraggableVoiceCallWindow.tsx +++ b/src/renderer/src/components/DraggableVoiceCallWindow.tsx @@ -2,11 +2,13 @@ import { AudioMutedOutlined, AudioOutlined, CloseOutlined, + DownOutlined, DragOutlined, PauseCircleOutlined, PlayCircleOutlined, SettingOutlined, - SoundOutlined + SoundOutlined, + UpOutlined } from '@ant-design/icons' import { Button, Space, Tooltip } from 'antd' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -178,6 +180,7 @@ const DraggableVoiceCallWindow: React.FC = ({ const [isSettingsVisible, setIsSettingsVisible] = useState(false) const [tempShortcutKey, setTempShortcutKey] = useState(shortcutKey) const [isRecordingShortcut, setIsRecordingShortcut] = useState(false) + const [isCollapsed, setIsCollapsed] = useState(false) // --- 快捷键相关状态结束 --- const isInitializedRef = useRef(false) @@ -518,6 +521,12 @@ const DraggableVoiceCallWindow: React.FC = ({
{/* 应用样式类 */} {t('voice_call.title')} +
- {isSettingsVisible && ( - - {' '} - {/* 使用 styled-component */} - {t('voice_call.shortcut_key_setting')} {/* 使用 styled-component */} - - setIsRecordingShortcut(true)}> + {!isCollapsed && ( + <> + {isSettingsVisible && ( + {' '} {/* 使用 styled-component */} - {isRecordingShortcut ? t('voice_call.press_any_key') : getKeyDisplayName(tempShortcutKey)} - - - - - - {' '} - {/* 使用 styled-component */} - {t('voice_call.shortcut_key_tip')} - - - )} - - - - + {t('voice_call.shortcut_key_setting')} {/* 使用 styled-component */} + + setIsRecordingShortcut(true)}> + {' '} + {/* 使用 styled-component */} + {isRecordingShortcut ? t('voice_call.press_any_key') : getKeyDisplayName(tempShortcutKey)} + + + + + + {' '} + {/* 使用 styled-component */} + {t('voice_call.shortcut_key_tip')} + + + )} + + + + - - {transcript && ( - - {t('voice_call.you')}: {transcript} - - )} - {/* 可以在这里添加 AI 回复的显示 */} - + + {transcript && ( + + {t('voice_call.you')}: {transcript} + + )} + {/* 可以在这里添加 AI 回复的显示 */} + + + )} diff --git a/src/renderer/src/components/TTSButton.tsx b/src/renderer/src/components/TTSButton.tsx index 9e70eb4cd1..085f630554 100644 --- a/src/renderer/src/components/TTSButton.tsx +++ b/src/renderer/src/components/TTSButton.tsx @@ -1,18 +1,37 @@ import { SoundOutlined } from '@ant-design/icons' import TTSService from '@renderer/services/TTSService' import { Message } from '@renderer/types' -import { Button, Tooltip } from 'antd' +import { Tooltip } from 'antd' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import styled from 'styled-components' interface TTSButtonProps { message: Message className?: string } +interface SegmentedPlaybackState { + isSegmentedPlayback: boolean + segments: { + text: string + isLoaded: boolean + isLoading: boolean + }[] + currentSegmentIndex: number + isPlaying: boolean +} + const TTSButton: React.FC = ({ message, className }) => { const { t } = useTranslation() const [isSpeaking, setIsSpeaking] = useState(false) + // 分段播放状态 + const [, setSegmentedPlaybackState] = useState({ + isSegmentedPlayback: false, + segments: [], + currentSegmentIndex: 0, + isPlaying: false + }) // 添加TTS状态变化事件监听器 useEffect(() => { @@ -31,6 +50,22 @@ const TTSButton: React.FC = ({ message, className }) => { } }, []) + // 监听分段播放状态变化 + useEffect(() => { + const handleSegmentedPlaybackUpdate = (event: CustomEvent) => { + console.log('检测到分段播放状态更新:', event.detail) + setSegmentedPlaybackState(event.detail) + } + + // 添加事件监听器 + window.addEventListener('tts-segmented-playback-update', handleSegmentedPlaybackUpdate as EventListener) + + // 组件卸载时移除事件监听器 + return () => { + window.removeEventListener('tts-segmented-playback-update', handleSegmentedPlaybackUpdate as EventListener) + } + }, []) + // 初始化时检查TTS状态 useEffect(() => { // 检查当前是否正在播放 @@ -38,7 +73,7 @@ const TTSButton: React.FC = ({ message, className }) => { if (isCurrentlyPlaying !== isSpeaking) { setIsSpeaking(isCurrentlyPlaying) } - }, []) + }, [isSpeaking]) const handleTTS = useCallback(async () => { if (isSpeaking) { @@ -57,16 +92,51 @@ const TTSButton: React.FC = ({ message, className }) => { } }, [isSpeaking, message]) + // 处理分段播放按钮点击 - 暂未使用,保留供未来扩展 + /* const handleSegmentedTTS = useCallback(async () => { + try { + console.log('点击分段TTS按钮,开始分段播放消息') + // 使用修改后的speakFromMessage方法,传入segmented=true参数 + await TTSService.speakFromMessage(message, true) + } catch (error) { + console.error('Segmented TTS error:', error) + } + }, [message]) */ + return ( - {t('settings.asr.local.browser_tip')} + + {/* 启动应用自动开启服务器 */} + + + dispatch(setAsrAutoStartServer(checked))} + disabled={!asrEnabled} + /> + {t('settings.asr.auto_start_server')} + + + + + diff --git a/src/renderer/src/services/TTSService.ts b/src/renderer/src/services/TTSService.ts index b99844254a..539acfba0d 100644 --- a/src/renderer/src/services/TTSService.ts +++ b/src/renderer/src/services/TTSService.ts @@ -17,9 +17,10 @@ class TTSService { /** * 将文本转换为语音并播放 * @param text 要转换的文本 + * @param segmented 是否使用分段播放 */ - speak = async (text: string): Promise => { - await this.service.speak(text) + speak = async (text: string, segmented: boolean = false): Promise => { + await this.service.speak(text, segmented) } /** @@ -32,9 +33,10 @@ class TTSService { /** * 从消息中提取文本并转换为语音 * @param message 消息对象 + * @param segmented 是否使用分段播放 */ - speakFromMessage = async (message: Message): Promise => { - await this.service.speakFromMessage(message) + speakFromMessage = async (message: Message, segmented: boolean = false): Promise => { + await this.service.speakFromMessage(message, segmented) } /** @@ -43,6 +45,22 @@ class TTSService { isCurrentlyPlaying = (): boolean => { return this.service.isCurrentlyPlaying() } + + /** + * 从指定段落开始播放 + * @param segmentIndex 段落索引 + */ + playFromSegment = (segmentIndex: number): void => { + this.service.playFromSegment(segmentIndex) + } + + /** + * 跳转到指定时间点 + * @param time 时间(秒) + */ + seek = (time: number): void => { + this.service.seek(time) + } } // 导出单例 diff --git a/src/renderer/src/services/tts/TTSService.ts b/src/renderer/src/services/tts/TTSService.ts index 7c5f8ed34e..d20c12577f 100644 --- a/src/renderer/src/services/tts/TTSService.ts +++ b/src/renderer/src/services/tts/TTSService.ts @@ -3,6 +3,7 @@ import store from '@renderer/store' import { setLastPlayedMessageId } from '@renderer/store/settings' import { Message } from '@renderer/types' +import { TextSegmenter } from './TextSegmenter' import { TTSServiceFactory } from './TTSServiceFactory' import { TTSTextFilter } from './TTSTextFilter' @@ -10,11 +11,27 @@ import { TTSTextFilter } from './TTSTextFilter' * TTS服务类 * 用于处理文本到语音的转换 */ +// 音频段落接口 +interface AudioSegment { + text: string // 段落文本 + audioBlob?: Blob // 对应的音频Blob + audioUrl?: string // 音频URL + isLoaded: boolean // 是否已加载 + isLoading: boolean // 是否正在加载 +} + export class TTSService { private static instance: TTSService private audioElement: HTMLAudioElement | null = null private isPlaying = false private playingServiceType: string | null = null + private playingMessageId: string | null = null + private progressUpdateInterval: NodeJS.Timeout | null = null + + // 分段播放相关属性 + private audioSegments: AudioSegment[] = [] + private currentSegmentIndex: number = 0 + private isSegmentedPlayback: boolean = false // 错误消息节流控制 private lastErrorTime = 0 @@ -76,9 +93,10 @@ export class TTSService { /** * 从消息中提取文本并播放 * @param message 消息对象 + * @param segmented 是否使用分段播放 * @returns 是否成功播放 */ - public async speakFromMessage(message: Message): Promise { + public async speakFromMessage(message: Message, segmented: boolean = false): Promise { // 获取最新的TTS过滤选项 const settings = store.getState().settings const ttsFilterOptions = settings.ttsFilterOptions || { @@ -94,12 +112,15 @@ export class TTSService { dispatch(setLastPlayedMessageId(message.id)) console.log('更新最后播放的消息ID:', message.id) + // 记录当前正在播放的消息ID + this.playingMessageId = message.id + // 应用过滤 const filteredText = TTSTextFilter.filterText(message.content, ttsFilterOptions) console.log('TTS过滤前文本长度:', message.content.length, '过滤后:', filteredText.length) // 播放过滤后的文本 - return this.speak(filteredText) + return this.speak(filteredText, segmented) } /** @@ -116,9 +137,15 @@ export class TTSService { const event = new CustomEvent('tts-state-change', { detail: { isPlaying } }) window.dispatchEvent(event) - // 如果停止播放,清除服务类型 + // 如果开始播放,启动进度更新定时器 + if (isPlaying && this.audioElement) { + this.startProgressUpdates() + } + + // 如果停止播放,清除服务类型和定时器 if (!isPlaying) { this.playingServiceType = null + this.stopProgressUpdates() // 确保Web Speech API也停止 if ('speechSynthesis' in window) { @@ -131,9 +158,10 @@ export class TTSService { /** * 播放文本 * @param text 要播放的文本 + * @param segmented 是否使用分段播放 * @returns 是否成功播放 */ - public async speak(text: string): Promise { + public async speak(text: string, segmented: boolean = false): Promise { try { // 检查TTS是否启用 const settings = store.getState().settings @@ -164,6 +192,15 @@ export class TTSService { console.log('使用的TTS服务类型:', serviceType) // 记录当前使用的服务类型 this.playingServiceType = serviceType + + // 设置分段播放模式 + this.isSegmentedPlayback = segmented + + if (segmented) { + // 分段播放模式 + return await this.speakSegmented(text, serviceType, latestSettings) + } + console.log('当前TTS设置详情:', { ttsServiceType: serviceType, ttsEdgeVoice: latestSettings.ttsEdgeVoice, @@ -264,8 +301,19 @@ export class TTSService { console.log('停止Web Speech API播放') } + // 停止进度更新 + this.stopProgressUpdates() + // 更新状态并触发事件 this.updatePlayingState(false) + + // 清除正在播放的消息ID + this.playingMessageId = null + + // 发送一个最终的进度更新事件,确保进度条消失 + this.emitProgressUpdateEvent(0, 0, 0) + + // 如果是分段播放模式,不清理资源,以便用户可以从其他段落继续播放 } /** @@ -276,6 +324,334 @@ export class TTSService { return this.isPlaying } + /** + * 跳转到指定时间点 + * @param time 时间(秒) + * @returns 是否成功跳转 + */ + public seek(time: number): boolean { + if (!this.audioElement || !this.isPlaying) { + console.log('无法跳转,音频元素不存在或未在播放中') + return false + } + + try { + // 确保时间在有效范围内 + const duration = this.audioElement.duration || 0 + const validTime = Math.max(0, Math.min(time, duration)) + + console.log(`跳转到时间点: ${validTime.toFixed(2)}秒 / ${duration.toFixed(2)}秒`) + this.audioElement.currentTime = validTime + return true + } catch (error) { + console.error('跳转失败:', error) + return false + } + } + + /** + * 从指定段落开始播放 + * @param index 段落索引 + * @returns 是否成功开始播放 + */ + public playFromSegment(index: number): boolean { + console.log(`请求从段落 ${index} 开始播放`) + + // 如果当前不是分段播放模式,则先将当前消息切换为分段播放模式 + if (!this.isSegmentedPlayback) { + console.log('当前不是分段播放模式,无法从指定段落开始播放') + return false + } + + if (index < 0 || index >= this.audioSegments.length) { + console.log(`段落索引超出范围: ${index}, 总段落数: ${this.audioSegments.length}`) + return false + } + + // 如果正在播放,先停止 + if (this.isPlaying) { + console.log('停止当前播放') + this.stop() + } + + console.log(`开始播放段落 ${index}: ${this.audioSegments[index].text.substring(0, 20)}...`) + + // 开始播放指定段落 + return this.playSegment(index) + } + + /** + * 分段播放模式 + * @param text 要播放的文本 + * @param serviceType TTS服务类型 + * @param settings 设置 + * @returns 是否成功播放 + */ + private async speakSegmented(text: string, serviceType: string, settings: any): Promise { + try { + console.log('开始分段播放模式') + + // 分割文本为句子 + const sentences = TextSegmenter.splitIntoSentences(text) + console.log(`文本分割为 ${sentences.length} 个段落`) + + if (sentences.length === 0) { + console.log('没有有效段落,取消播放') + return false + } + + // 创建音频段落数组 + this.audioSegments = sentences.map((sentence) => ({ + text: sentence, + isLoaded: false, + isLoading: false + })) + + // 设置分段播放模式 + this.isSegmentedPlayback = true + + // 重置当前段落索引 + this.currentSegmentIndex = 0 + + // 触发分段播放事件 + this.emitSegmentedPlaybackEvent() + + // 预加载所有段落,确保完整播放 + for (let i = 0; i < sentences.length; i++) { + // 使用setTimeout错开加载时间,避免同时发起过多请求 + setTimeout(() => { + if (this.isSegmentedPlayback) { + // 确保仍然在分段播放模式 + this.loadSegmentAudio(i, serviceType, settings) + } + }, i * 100) // 每100毫秒加载一个段落 + } + + // 不自动开始播放,等待用户点击 + console.log('分段播放模式已准备就绪,等待用户点击') + return true + } catch (error: any) { + console.error('TTS分段播放失败:', error) + this.showErrorMessage(error?.message || i18n.t('settings.tts.error.synthesis_failed')) + return false + } + } + + /** + * 加载段落音频 + * @param index 段落索引 + * @param serviceType TTS服务类型 + * @param settings 设置 + */ + private async loadSegmentAudio(index: number, serviceType: string, settings: any): Promise { + if (index < 0 || index >= this.audioSegments.length) { + return + } + + const segment = this.audioSegments[index] + + // 如果已加载或正在加载,则跳过 + if (segment.isLoaded || segment.isLoading) { + return + } + + // 标记为正在加载 + segment.isLoading = true + this.emitSegmentedPlaybackEvent() + + try { + // 创建TTS服务 + const ttsService = TTSServiceFactory.createService(serviceType, settings) + + // 合成音频 + const audioBlob = await ttsService.synthesize(segment.text) + + // 创建音频URL + const audioUrl = URL.createObjectURL(audioBlob) + + // 更新段落信息 + segment.audioBlob = audioBlob + segment.audioUrl = audioUrl + segment.isLoaded = true + segment.isLoading = false + + // 触发事件 + this.emitSegmentedPlaybackEvent() + + // 如果是当前播放的段落,且尚未开始播放,则开始播放 + if (index === this.currentSegmentIndex && this.isSegmentedPlayback && !this.isPlaying) { + this.playSegment(index) + } + } catch (error) { + console.error(`加载段落音频失败 (索引: ${index}):`, error) + segment.isLoading = false + this.emitSegmentedPlaybackEvent() + } + } + + /** + * 播放指定段落 + * @param index 段落索引 + * @returns 是否成功开始播放 + */ + private playSegment(index: number): boolean { + if (index < 0 || index >= this.audioSegments.length) { + return false + } + + const segment = this.audioSegments[index] + + // 如果段落尚未加载完成,则等待加载 + if (!segment.isLoaded) { + // 如果尚未开始加载,则开始加载 + if (!segment.isLoading) { + const settings = store.getState().settings + const serviceType = settings.ttsServiceType || 'openai' + this.loadSegmentAudio(index, serviceType, settings) + } + return true // 返回true表示已开始处理,但尚未实际播放 + } + + // 更新当前段落索引 + this.currentSegmentIndex = index + + // 触发事件 + this.emitSegmentedPlaybackEvent() + + // 播放音频 + if (this.audioElement && segment.audioUrl) { + this.audioElement.src = segment.audioUrl + this.audioElement.play().catch((error) => { + console.error('播放段落音频失败:', error) + }) + + // 更新播放状态 + this.updatePlayingState(true) + + // 设置音频结束事件 + this.audioElement.onended = () => { + // 播放下一个段落 + if (index < this.audioSegments.length - 1) { + this.playSegment(index + 1) + } else { + // 所有段落播放完毕 + this.updatePlayingState(false) + + // 清理资源 + this.cleanupSegmentedPlayback() + } + } + + return true + } + + return false + } + + /** + * 清理分段播放资源 + */ + private cleanupSegmentedPlayback(): void { + // 释放所有音频URL + for (const segment of this.audioSegments) { + if (segment.audioUrl) { + URL.revokeObjectURL(segment.audioUrl) + } + } + + // 重置状态 + this.audioSegments = [] + this.currentSegmentIndex = 0 + this.isSegmentedPlayback = false + + // 触发事件 + this.emitSegmentedPlaybackEvent() + + console.log('分段播放已完成,资源已清理') + } + + /** + * 启动进度更新定时器 + */ + private startProgressUpdates(): void { + // 先停止现有的定时器 + this.stopProgressUpdates() + + // 确保音频元素存在 + if (!this.audioElement) return + + // 创建新的定时器,每100毫秒更新一次进度 + this.progressUpdateInterval = setInterval(() => { + if (this.audioElement && this.isPlaying) { + const currentTime = this.audioElement.currentTime + const duration = this.audioElement.duration || 0 + + // 计算进度百分比 + const progress = duration > 0 ? (currentTime / duration) * 100 : 0 + + // 触发进度更新事件 + this.emitProgressUpdateEvent(currentTime, duration, progress) + } + }, 100) + } + + /** + * 停止进度更新定时器 + */ + private stopProgressUpdates(): void { + if (this.progressUpdateInterval) { + clearInterval(this.progressUpdateInterval) + this.progressUpdateInterval = null + } + } + + /** + * 触发进度更新事件 + * @param currentTime 当前播放时间(秒) + * @param duration 总时长(秒) + * @param progress 进度百分比(0-100) + */ + private emitProgressUpdateEvent(currentTime: number, duration: number, progress: number): void { + // 创建事件数据 + const eventData = { + messageId: this.playingMessageId, + isPlaying: this.isPlaying, + currentTime, + duration, + progress + } + + console.log('发送TTS进度更新事件:', { + messageId: this.playingMessageId, + progress: Math.round(progress), + currentTime: Math.round(currentTime), + duration: Math.round(duration) + }) + + // 触发事件 + window.dispatchEvent(new CustomEvent('tts-progress-update', { detail: eventData })) + } + + /** + * 触发分段播放事件 + */ + private emitSegmentedPlaybackEvent(): void { + // 创建事件数据 + const eventData = { + isSegmentedPlayback: this.isSegmentedPlayback, + segments: this.audioSegments.map((segment) => ({ + text: segment.text, + isLoaded: segment.isLoaded, + isLoading: segment.isLoading + })), + currentSegmentIndex: this.currentSegmentIndex, + isPlaying: this.isPlaying + } + + // 触发事件 + window.dispatchEvent(new CustomEvent('tts-segmented-playback-update', { detail: eventData })) + } + /** * 显示错误消息,并进行节流控制 * @param message 错误消息 diff --git a/src/renderer/src/services/tts/TextSegmenter.ts b/src/renderer/src/services/tts/TextSegmenter.ts new file mode 100644 index 0000000000..b999bbdeeb --- /dev/null +++ b/src/renderer/src/services/tts/TextSegmenter.ts @@ -0,0 +1,85 @@ +/** + * 文本分段工具类 + * 用于将文本分割成句子或段落 + */ +export class TextSegmenter { + /** + * 将文本分割成句子 + * @param text 要分割的文本 + * @returns 句子数组 + */ + public static splitIntoSentences(text: string): string[] { + if (!text || text.trim() === '') { + return [] + } + + // 以句子级别的标点符号为主要分隔点,保证流畅性 + // 句号、问号、感叹号、分号作为主要分隔点 + const punctuationRegex = /([.;:?!。;:?!]+)/g + + // 分割文本 + const parts = text.split(punctuationRegex) + const segments: string[] = [] + + // 将标点符号与前面的文本组合 + for (let i = 0; i < parts.length - 1; i += 2) { + const content = parts[i] + const punctuation = parts[i + 1] || '' + if (content.trim() || punctuation.trim()) { + segments.push((content.trim() + punctuation.trim()).trim()) + } + } + + // 处理最后一个部分(如果有) + if (parts.length % 2 !== 0 && parts[parts.length - 1].trim()) { + segments.push(parts[parts.length - 1].trim()) + } + + // 进一步处理空格和换行符 + const result = segments + .filter((segment) => segment.trim().length > 0) // 过滤空的片段 + .flatMap((segment) => { + // 如果片段过长,按换行符进一步分割 + if (segment.length > 100) { + const subParts = segment.split(/([\n\r]+)/) + const subSegments: string[] = [] + + for (let i = 0; i < subParts.length; i++) { + const part = subParts[i].trim() + if (part) { + subSegments.push(part) + } + } + + return subSegments.length > 0 ? subSegments : [segment] + } + + return [segment] + }) + + // 合并过短的片段,保证流畅性 + const mergedResult: string[] = [] + let currentSegment = '' + + for (const segment of result) { + // 如果当前片段加上新片段仍然不超过100个字符,则合并 + if (currentSegment && currentSegment.length + segment.length < 100) { + currentSegment += ' ' + segment + } else { + // 如果当前片段非空,则添加到结果中 + if (currentSegment) { + mergedResult.push(currentSegment) + } + currentSegment = segment + } + } + + // 添加最后一个片段 + if (currentSegment) { + mergedResult.push(currentSegment) + } + + // 如果没有成功分割,则返回原文本作为一个句子 + return mergedResult.length > 0 ? mergedResult : [text] + } +} diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 84512f2226..6377049dd3 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -145,6 +145,7 @@ export interface SettingsState { asrApiKey: string asrApiUrl: string asrModel: string + asrAutoStartServer: boolean // 启动应用时自动启动ASR服务器 // 语音通话配置 voiceCallEnabled: boolean voiceCallModel: Model | null @@ -286,6 +287,7 @@ export const initialState: SettingsState = { asrApiKey: '', asrApiUrl: 'https://api.openai.com/v1/audio/transcriptions', asrModel: 'whisper-1', + asrAutoStartServer: false, // 默认不自动启动ASR服务器 // 语音通话配置 voiceCallEnabled: true, voiceCallModel: null, @@ -714,6 +716,9 @@ const settingsSlice = createSlice({ setAsrModel: (state, action: PayloadAction) => { state.asrModel = action.payload }, + setAsrAutoStartServer: (state, action: PayloadAction) => { + state.asrAutoStartServer = action.payload + }, setVoiceCallEnabled: (state, action: PayloadAction) => { state.voiceCallEnabled = action.payload }, @@ -851,6 +856,7 @@ export const { setAsrApiKey, setAsrApiUrl, setAsrModel, + setAsrAutoStartServer, setVoiceCallEnabled, setVoiceCallModel, setIsVoiceCallActive,