From 1e0e1127c71606db5610fddd21edbe6347890788 Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:43:12 +0800 Subject: [PATCH 01/59] Add files via upload TTS --- src/renderer/src/components/TTSButton.tsx | 64 + src/renderer/src/i18n/locales/en-us.json | 43 + src/renderer/src/i18n/locales/zh-cn.json | 43 + .../pages/home/Messages/MessageMenubar.tsx | 75 ++ .../src/pages/home/Messages/Messages.tsx | 3 + .../src/pages/home/Messages/TTSStopButton.tsx | 73 ++ .../settings/TTSSettings/TTSSettings.tsx | 705 +++++++++++ src/renderer/src/services/TTSService.ts | 1102 +++++++++++++++++ src/renderer/src/store/settings.ts | 167 ++- src/renderer/src/types/index.ts | 9 + 10 files changed, 2282 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/TTSButton.tsx create mode 100644 src/renderer/src/pages/home/Messages/TTSStopButton.tsx create mode 100644 src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx create mode 100644 src/renderer/src/services/TTSService.ts diff --git a/src/renderer/src/components/TTSButton.tsx b/src/renderer/src/components/TTSButton.tsx new file mode 100644 index 0000000000..36b856a434 --- /dev/null +++ b/src/renderer/src/components/TTSButton.tsx @@ -0,0 +1,64 @@ +import { SoundOutlined } from '@ant-design/icons' +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 + className?: string +} + +const TTSButton: React.FC = ({ message, className }) => { + const { t } = useTranslation() + const [isSpeaking, setIsSpeaking] = useState(false) + + const handleTTS = useCallback(async () => { + if (isSpeaking) { + TTSService.stop() + setIsSpeaking(false) + return + } + + setIsSpeaking(true) + try { + await TTSService.speakFromMessage(message) + + // 监听播放结束 + const checkPlayingStatus = () => { + if (!TTSService.isCurrentlyPlaying()) { + setIsSpeaking(false) + clearInterval(checkInterval) + } + } + + const checkInterval = setInterval(checkPlayingStatus, 500) + + // 安全机制,确保即使出错也会重置状态 + setTimeout(() => { + if (isSpeaking) { + TTSService.stop() + setIsSpeaking(false) + clearInterval(checkInterval) + } + }, 30000) // 30秒后检查 + } catch (error) { + console.error('TTS error:', error) + setIsSpeaking(false) + } + }, [isSpeaking, message]) + + return ( + + + + {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} + /> + + + )} + + {/* Edge 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')}: + dispatch(setTtsEdgeVoice(value))} - options={availableVoices.length > 0 ? availableVoices : [ - { label: t('settings.tts.edge_voice.loading'), value: '' } - ]} + options={ + availableVoices.length > 0 + ? availableVoices + : [{ label: t('settings.tts.edge_voice.loading'), value: '' }] + } disabled={!ttsEnabled} style={{ flex: 1 }} showSearch optionFilterProp="label" - placeholder={availableVoices.length === 0 ? t('settings.tts.edge_voice.loading') : t('settings.tts.voice.placeholder')} - notFoundContent={availableVoices.length === 0 ? t('settings.tts.edge_voice.loading') : t('settings.tts.edge_voice.not_found')} + placeholder={ + availableVoices.length === 0 + ? t('settings.tts.edge_voice.loading') + : t('settings.tts.voice.placeholder') + } + notFoundContent={ + availableVoices.length === 0 + ? t('settings.tts.edge_voice.loading') + : t('settings.tts.edge_voice.not_found') + } /> @@ -566,8 +590,8 @@ const TTSSettings: FC = () => { onChange={(value) => dispatch(setTtsModel(value))} options={ttsCustomModels.map((model: any) => { // 确保model是字符串 - const modelStr = typeof model === 'string' ? model : String(model); - return { label: modelStr, value: modelStr }; + const modelStr = typeof model === 'string' ? model : String(model) + return { label: modelStr, value: modelStr } })} disabled={!ttsEnabled} style={{ width: '100%' }} @@ -583,22 +607,19 @@ const TTSSettings: FC = () => { {ttsCustomModels && ttsCustomModels.length > 0 ? ( ttsCustomModels.map((model: any, index: number) => { // 确保model是字符串 - const modelStr = typeof model === 'string' ? model : String(model); + const modelStr = typeof model === 'string' ? model : String(model) return ( handleRemoveModel(modelStr)} - style={{ padding: '4px 8px' }} - > + style={{ padding: '4px 8px' }}> {modelStr} - ); + ) }) ) : ( - - {t('settings.tts.model_empty')} - + {t('settings.tts.model_empty')} )} @@ -616,8 +637,7 @@ const TTSSettings: FC = () => { type="primary" icon={} onClick={handleAddModel} - disabled={!ttsEnabled || !newModel} - > + disabled={!ttsEnabled || !newModel}> {t('settings.tts.model_add')} @@ -632,28 +652,32 @@ const TTSSettings: FC = () => { checked={ttsFilterOptions.filterThinkingProcess} onChange={(checked) => dispatch(setTtsFilterOptions({ filterThinkingProcess: checked }))} disabled={!ttsEnabled} - /> {t('settings.tts.filter.thinking_process')} + />{' '} + {t('settings.tts.filter.thinking_process')} dispatch(setTtsFilterOptions({ filterMarkdown: checked }))} disabled={!ttsEnabled} - /> {t('settings.tts.filter.markdown')} + />{' '} + {t('settings.tts.filter.markdown')} dispatch(setTtsFilterOptions({ filterCodeBlocks: checked }))} disabled={!ttsEnabled} - /> {t('settings.tts.filter.code_blocks')} + />{' '} + {t('settings.tts.filter.code_blocks')} dispatch(setTtsFilterOptions({ filterHtmlTags: checked }))} disabled={!ttsEnabled} - /> {t('settings.tts.filter.html_tags')} + />{' '} + {t('settings.tts.filter.html_tags')} {t('settings.tts.max_text_length')}: @@ -667,7 +691,7 @@ const TTSSettings: FC = () => { { label: '2000', value: 2000 }, { label: '4000', value: 4000 }, { label: '8000', value: 8000 }, - { label: '16000', value: 16000 }, + { label: '16000', value: 16000 } ]} /> @@ -681,8 +705,7 @@ const TTSSettings: FC = () => { !ttsEnabled || (ttsServiceType === 'openai' && (!ttsApiKey || !ttsVoice || !ttsModel)) || (ttsServiceType === 'edge' && !ttsEdgeVoice) - } - > + }> {t('settings.tts.test')} From 5d01e89679d5938e5b4042e0da532b5968b68138 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 9 Apr 2025 18:14:28 +0800 Subject: [PATCH 15/59] refactor: Update TTS settings and improve localization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed the title in the Chinese localization from "语音合成设置" to "语音设置" for clarity. - Adjusted the layout of the TTSSettings component, including margin and flex properties for better alignment. - Enhanced the help text section to improve readability and structure. - Updated comments in settings.ts for better understanding of default values. --- src/renderer/src/i18n/locales/zh-cn.json | 2 +- .../settings/TTSSettings/TTSSettings.tsx | 20 +++++++------------ src/renderer/src/store/settings.ts | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 58e092d8a3..3961f6dc75 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1336,7 +1336,7 @@ "enable_privacy_mode": "匿名发送错误报告和数据统计" }, "tts": { - "title": "语音合成设置", + "title": "语音设置", "enable": "启用语音合成", "enable.help": "启用后可以将文本转换为语音", "reset": "重置", diff --git a/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx index df350d00fa..74b3aafc08 100644 --- a/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx +++ b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx @@ -34,7 +34,6 @@ import { } from '..' const CustomVoiceInput = styled.div` - margin-top: 16px; display: flex; flex-direction: column; gap: 8px; @@ -66,6 +65,10 @@ const FlexContainer = styled.div` const FilterOptionItem = styled.div` margin-bottom: 16px; + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; ` const LengthLabel = styled.span` @@ -378,9 +381,7 @@ const TTSSettings: FC = () => { {t('settings.tts.title')} - - {t('settings.tts.enable')} @@ -406,12 +407,9 @@ const TTSSettings: FC = () => { {t('settings.tts.reset_help')} - - - {t('settings.tts.api_settings')} - + {/* TTS服务类型选择 */} @@ -711,12 +709,8 @@ const TTSSettings: FC = () => { - - - - - {t('settings.tts.help')} -
+ + {t('settings.tts.help')} {t('settings.tts.learn_more')} diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index cb51b792b9..bc3340eea6 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -232,7 +232,7 @@ export const initialState: SettingsState = { enableDataCollection: false, // TTS配置 ttsEnabled: false, - ttsServiceType: 'openai', // 默认使用OpenAI TTS + ttsServiceType: 'openai', // 默认使用 OpenAI TTS ttsApiKey: '', ttsApiUrl: 'https://api.openai.com/v1/audio/speech', ttsVoice: '', From dc5a7052b0b012120ba3520c77532f46296100ea Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:50:43 +0800 Subject: [PATCH 16/59] Add files via upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CSP策略 --- src/renderer/index.html | 73 +++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/renderer/index.html b/src/renderer/index.html index 19a44594df..973a6656c8 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -1,42 +1,43 @@ - - - - - Cherry Studio - - + #spinner { + position: fixed; + width: 100vw; + height: 100vh; + flex-direction: row; + justify-content: center; + align-items: center; + display: none; + } - -
-
- -
- - - - + #spinner img { + width: 100px; + border-radius: 50px; + } + + + + +
+
+ +
+ + + + + \ No newline at end of file From ca5a6b322c1685fd7e844095eb54c97cd72f53c6 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Wed, 9 Apr 2025 20:17:46 +0800 Subject: [PATCH 17/59] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E8=BE=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/home/Messages/MessageMenubar.tsx | 6 +++- .../src/pages/home/Messages/TTSStopButton.tsx | 32 +++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index b44dcb86d4..c66718539c 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -399,7 +399,11 @@ const MessageMenubar: FC = (props) => {
)} {isAssistantMessage && ttsEnabled && ( - + + TTSService.speakFromMessage(message)}> + + + )} {!isUserMessage && ( { return ( - } - onClick={handleStopTTS} - size="large" - /> + + + ) @@ -66,8 +62,26 @@ const StopButtonContainer = styled.div` z-index: 1000; ` -const StyledButton = styled(Button)` +const ActionButton = styled.div` + cursor: pointer; + border-radius: 8px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 30px; + height: 30px; + transition: all 0.2s ease; + background-color: var(--color-primary); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + &:hover { + background-color: var(--color-primary-soft); + } + .anticon { + cursor: pointer; + font-size: 14px; + color: var(--color-white); + } ` export default TTSStopButton From 4a2f1d5cf6016aba8011ba81b4d462eeb6b6e3f9 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Wed, 9 Apr 2025 20:56:27 +0800 Subject: [PATCH 18/59] =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/i18n/locales/ja-jp.json | 29 ++++++++++++++++++++++++ src/renderer/src/i18n/locales/ru-ru.json | 29 ++++++++++++++++++++++++ src/renderer/src/i18n/locales/zh-tw.json | 29 ++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index a7908d3767..4b94f9793b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -103,6 +103,13 @@ "default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。", "default.name": "デフォルトアシスタント", "default.topic.name": "デフォルトトピック", + "tts": { + "play": "音声を再生", + "stop": "再生を停止", + "speak": "音声を再生", + "stop_global": "すべての音声再生を停止", + "stopped": "音声再生を停止しました" + }, "history": { "assistant_node": "アシスタント", "click_to_navigate": "メッセージに移動", @@ -1327,6 +1334,28 @@ "privacy": { "title": "プライバシー設定", "enable_privacy_mode": "匿名エラーレポートとデータ統計の送信" + }, + "tts": { + "title": "音声合成設定", + "enable": "音声合成を有効にする", + "enable.help": "テキストを音声に変換する機能を有効にします", + "reset": "リセット", + "reset_title": "カスタム音声とモデルをリセット", + "reset_confirm": "すべてのカスタム音声とモデルをリセットしますか?追加したすべてのカスタム項目が削除されます。", + "reset_success": "リセットに成功しました", + "reset_help": "音声やモデルの表示に異常がある場合は、すべてのカスタム項目をリセットしてみてください", + "api_settings": "API設定", + "service_type": "サービスタイプ", + "service_type.openai": "OpenAI", + "service_type.edge": "Edge TTS", + "test": "テスト", + "error": { + "failed": "音声合成に失敗しました", + "not_enabled": "音声合成が有効になっていません", + "no_edge_voice": "Edge TTSの音声が選択されていません" + }, + "help": "OpenAIのTTS APIを使用するには、APIキーが必要です。Edge TTSはブラウザの機能を使用するため、APIキーは不要です。", + "learn_more": "詳細はこちら" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 54e7b6a71a..9c9419bab2 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -103,6 +103,13 @@ "default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас", "default.name": "Ассистент по умолчанию", "default.topic.name": "Топик по умолчанию", + "tts": { + "play": "Воспроизвести речь", + "stop": "Остановить воспроизведение", + "speak": "Воспроизвести речь", + "stop_global": "Остановить все воспроизведение речи", + "stopped": "Воспроизведение речи остановлено" + }, "history": { "assistant_node": "Ассистент", "click_to_navigate": "Перейти к сообщению", @@ -1327,6 +1334,28 @@ "privacy": { "title": "Настройки приватности", "enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики" + }, + "tts": { + "title": "Настройки преобразования текста в речь", + "enable": "Включить преобразование текста в речь", + "enable.help": "Включить функцию преобразования текста в речь", + "reset": "Сбросить", + "reset_title": "Сбросить пользовательские голоса и модели", + "reset_confirm": "Вы уверены, что хотите сбросить все пользовательские голоса и модели? Это удалит все добавленные вами пользовательские элементы.", + "reset_success": "Сброс выполнен успешно", + "reset_help": "Если голоса или модели отображаются некорректно, попробуйте сбросить все пользовательские элементы", + "api_settings": "Настройки API", + "service_type": "Тип сервиса", + "service_type.openai": "OpenAI", + "service_type.edge": "Edge TTS", + "test": "Тест", + "error": { + "failed": "Не удалось преобразовать текст в речь", + "not_enabled": "Преобразование текста в речь не включено", + "no_edge_voice": "Голос Edge TTS не выбран" + }, + "help": "Для использования API TTS OpenAI требуется ключ API. Edge TTS использует функции браузера и не требует ключа API.", + "learn_more": "Узнать больше" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index a2b6ed2057..b50f72e3f6 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -103,6 +103,13 @@ "default.description": "你好,我是預設助手。你可以立即開始與我聊天。", "default.name": "預設助手", "default.topic.name": "預設話題", + "tts": { + "play": "播放語音", + "stop": "停止播放", + "speak": "播放語音", + "stop_global": "停止所有語音播放", + "stopped": "已停止語音播放" + }, "history": { "assistant_node": "助手", "click_to_navigate": "點擊跳轉到對應訊息", @@ -1327,6 +1334,28 @@ "privacy": { "title": "隱私設定", "enable_privacy_mode": "匿名發送錯誤報告和資料統計" + }, + "tts": { + "title": "語音設定", + "enable": "啟用語音合成", + "enable.help": "啟用後可以將文字轉換為語音", + "reset": "重置", + "reset_title": "重置自定義音色和模型", + "reset_confirm": "確定要重置所有自定義音色和模型嗎?這將刪除所有已添加的自定義項。", + "reset_success": "重置成功", + "reset_help": "如果音色或模型顯示異常,可以嘗試重置所有自定義項", + "api_settings": "API設定", + "service_type": "服務類型", + "service_type.openai": "OpenAI", + "service_type.edge": "Edge TTS", + "test": "測試", + "error": { + "failed": "語音合成失敗", + "not_enabled": "語音合成未啟用", + "no_edge_voice": "未選擇Edge TTS音色" + }, + "help": "使用OpenAI的TTS API需要API金鑰。Edge TTS使用瀏覽器功能,不需要API金鑰。", + "learn_more": "了解更多" } }, "translate": { From bbe08e2a6cf09fecf587ef4468f8d17f261cc7fb Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Thu, 10 Apr 2025 12:30:22 +0800 Subject: [PATCH 19/59] ASR-TTS --- electron-builder.yml | 5 + electron.vite.config.ts | 11 + public/asr-server/index.html | 198 ++++++ public/asr-server/server.js | 146 ++++ src/main/ipc.ts | 104 +++ src/renderer/src/assets/asr-server/index.html | 368 ++++++++++ .../src/assets/asr-server/package.json | 27 + src/renderer/src/assets/asr-server/server.js | 172 +++++ src/renderer/src/components/ASRButton.tsx | 226 ++++++ src/renderer/src/i18n/locales/en-us.json | 46 ++ src/renderer/src/i18n/locales/ja-jp.json | 46 ++ src/renderer/src/i18n/locales/zh-cn.json | 64 +- .../src/pages/home/Inputbar/Inputbar.tsx | 14 + .../pages/home/Messages/MessageMenubar.tsx | 14 +- .../src/pages/settings/SettingsPage.tsx | 2 +- .../settings/TTSSettings/ASRSettings.tsx | 271 +++++++ .../settings/TTSSettings/TTSSettings.tsx | 668 +++++++++--------- src/renderer/src/services/ASRServerService.ts | 129 ++++ src/renderer/src/services/ASRService.ts | 560 +++++++++++++++ src/renderer/src/store/settings.ts | 35 +- src/renderer/src/types/electron.d.ts | 15 + 21 files changed, 2790 insertions(+), 331 deletions(-) create mode 100644 public/asr-server/index.html create mode 100644 public/asr-server/server.js create mode 100644 src/renderer/src/assets/asr-server/index.html create mode 100644 src/renderer/src/assets/asr-server/package.json create mode 100644 src/renderer/src/assets/asr-server/server.js create mode 100644 src/renderer/src/components/ASRButton.tsx create mode 100644 src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx create mode 100644 src/renderer/src/services/ASRServerService.ts create mode 100644 src/renderer/src/services/ASRService.ts diff --git a/electron-builder.yml b/electron-builder.yml index 7cc8ffe1f9..d45562fb86 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -27,6 +27,11 @@ files: - '!node_modules/@tavily/core/node_modules/js-tiktoken' - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' + # 包含 ASR 服务器文件 + - src/renderer/src/assets/asr-server/**/* + # 包含打包后的ASR服务器可执行文件 + - cherry-asr-server.exe + - index.html asarUnpack: - resources/** - '**/*.{node,dll,metal,exp,lib}' diff --git a/electron.vite.config.ts b/electron.vite.config.ts index d0ceafc025..3a52c4ddcb 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -76,6 +76,17 @@ export default defineConfig({ }, optimizeDeps: { exclude: [] + }, + build: { + rollupOptions: { + input: { + index: resolve('src/renderer/index.html'), + }, + }, + // 复制ASR服务器文件 + assetsInlineLimit: 0, + // 确保复制assets目录下的所有文件 + copyPublicDir: true, } } }) diff --git a/public/asr-server/index.html b/public/asr-server/index.html new file mode 100644 index 0000000000..9cd22e0b5d --- /dev/null +++ b/public/asr-server/index.html @@ -0,0 +1,198 @@ + + + + + + + Edge ASR (External) + + + + +

Edge ASR 中继页面

+

这个页面需要在 Edge 浏览器中保持打开,以便 Electron 应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/public/asr-server/server.js b/public/asr-server/server.js new file mode 100644 index 0000000000..dca76b894c --- /dev/null +++ b/public/asr-server/server.js @@ -0,0 +1,146 @@ +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module + +const app = express() +const port = 8080 // Define the port + +// 提供网页给 Edge 浏览器 +app.get('/', (req, res) => { + // Use path.join for cross-platform compatibility + res.sendFile(path.join(__dirname, 'index.html')) +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = 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'); + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = 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'); + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) +}) + +// Handle server errors +server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start +}) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index c6bceac3ab..47c13b0ef7 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,4 +1,6 @@ import fs from 'node:fs' +import { spawn, ChildProcess } from 'node:child_process' +import path from 'node:path' import { isMac, isWin } from '@main/constant' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' @@ -29,6 +31,9 @@ import { decrypt, encrypt } from './utils/aes' import { getConfigDir, getFilesDir } from './utils/file' import { compress, decompress } from './utils/zip' +// 存储ASR服务器进程 +let asrServerProcess: ChildProcess | null = null + const fileManager = new FileStorage() const backupManager = new BackupManager() const exportService = new ExportService(fileManager) @@ -291,4 +296,103 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.Nutstore_GetDirectoryContents, (_, token: string, path: string) => NutstoreService.getDirectoryContents(token, path) ) + + // 启动ASR服务器 + ipcMain.handle('start-asr-server', async () => { + try { + if (asrServerProcess) { + return { success: true, pid: asrServerProcess.pid } + } + + // 获取服务器文件路径 + console.log('App path:', app.getAppPath()) + // 在开发环境和生产环境中使用不同的路径 + let serverPath = '' + let isExeFile = false + + // 首先检查是否有打包后的exe文件 + const exePath = path.join(app.getAppPath(), 'resources', 'cherry-asr-server.exe') + if (fs.existsSync(exePath)) { + serverPath = exePath + isExeFile = true + console.log('检测到打包后的exe文件:', serverPath) + } else if (process.env.NODE_ENV === 'development') { + // 开发环境 + serverPath = path.join(app.getAppPath(), 'src', 'renderer', 'src', 'assets', 'asr-server', 'server.js') + } else { + // 生产环境 + serverPath = path.join(app.getAppPath(), 'public', 'asr-server', 'server.js') + } + console.log('ASR服务器路径:', serverPath) + + // 检查文件是否存在 + if (!fs.existsSync(serverPath)) { + return { success: false, error: '服务器文件不存在' } + } + + // 启动服务器进程 + if (isExeFile) { + // 如果是exe文件,直接启动 + asrServerProcess = spawn(serverPath, [], { + stdio: 'pipe', + detached: false + }) + } else { + // 如果是js文件,使用node启动 + asrServerProcess = spawn('node', [serverPath], { + stdio: 'pipe', + detached: false + }) + } + + // 处理服务器输出 + asrServerProcess.stdout?.on('data', (data) => { + console.log(`[ASR Server] ${data.toString()}`) + }) + + asrServerProcess.stderr?.on('data', (data) => { + console.error(`[ASR Server Error] ${data.toString()}`) + }) + + // 处理服务器退出 + asrServerProcess.on('close', (code) => { + console.log(`[ASR Server] 进程退出,退出码: ${code}`) + asrServerProcess = null + }) + + // 等待一段时间确保服务器启动 + await new Promise(resolve => setTimeout(resolve, 1000)) + + return { success: true, pid: asrServerProcess.pid } + } catch (error) { + console.error('启动ASR服务器失败:', error) + return { success: false, error: (error as Error).message } + } + }) + + // 停止ASR服务器 + ipcMain.handle('stop-asr-server', async (_event, pid) => { + try { + if (!asrServerProcess) { + return { success: true } + } + + // 检查PID是否匹配 + if (asrServerProcess.pid !== pid) { + console.warn(`请求停止的PID (${pid}) 与当前运行的ASR服务器PID (${asrServerProcess.pid}) 不匹配`) + } + + // 杀死进程 + asrServerProcess.kill() + + // 等待一段时间确保进程已经退出 + await new Promise(resolve => setTimeout(resolve, 500)) + + asrServerProcess = null + return { success: true } + } catch (error) { + console.error('停止ASR服务器失败:', error) + return { success: false, error: (error as Error).message } + } + }) } diff --git a/src/renderer/src/assets/asr-server/index.html b/src/renderer/src/assets/asr-server/index.html new file mode 100644 index 0000000000..1d5d20e5ca --- /dev/null +++ b/src/renderer/src/assets/asr-server/index.html @@ -0,0 +1,368 @@ + + + + + + + Edge ASR (External) + + + + +

Edge ASR 中继页面

+

这个页面需要在 Edge 浏览器中保持打开,以便 Electron 应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/src/renderer/src/assets/asr-server/package.json b/src/renderer/src/assets/asr-server/package.json new file mode 100644 index 0000000000..75eaaadcb4 --- /dev/null +++ b/src/renderer/src/assets/asr-server/package.json @@ -0,0 +1,27 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "description": "Cherry Studio ASR Server", + "main": "server.js", + "bin": "server.js", + "scripts": { + "start": "node server.js", + "build": "pkg ." + }, + "pkg": { + "targets": [ + "node16-win-x64" + ], + "outputPath": "dist", + "assets": [ + "index.html" + ] + }, + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + }, + "devDependencies": { + "pkg": "^5.8.1" + } +} diff --git a/src/renderer/src/assets/asr-server/server.js b/src/renderer/src/assets/asr-server/server.js new file mode 100644 index 0000000000..ac57acb360 --- /dev/null +++ b/src/renderer/src/assets/asr-server/server.js @@ -0,0 +1,172 @@ +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module + +const app = express() +const port = 8080 // Define the port + +// 获取index.html文件的路径 +function getIndexHtmlPath() { + // 在开发环境中,直接使用相对路径 + const devPath = path.join(__dirname, 'index.html'); + + // 在pkg打包后,文件会被包含在可执行文件中 + // 使用process.pkg检测是否是打包环境 + if (process.pkg) { + // 在打包环境中,使用绝对路径 + return path.join(path.dirname(process.execPath), 'index.html'); + } + + // 如果文件存在,返回开发路径 + try { + if (require('fs').existsSync(devPath)) { + return devPath; + } + } catch (e) { + console.error('Error checking file existence:', e); + } + + // 如果都不存在,尝试使用当前目录 + return path.join(process.cwd(), 'index.html'); +} + +// 提供网页给 Edge 浏览器 +app.get('/', (req, res) => { + const indexPath = getIndexHtmlPath(); + console.log(`Serving index.html from: ${indexPath}`); + res.sendFile(indexPath); +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = 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'); + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = 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'); + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) +}) + +// Handle server errors +server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start +}) diff --git a/src/renderer/src/components/ASRButton.tsx b/src/renderer/src/components/ASRButton.tsx new file mode 100644 index 0000000000..67813a9af1 --- /dev/null +++ b/src/renderer/src/components/ASRButton.tsx @@ -0,0 +1,226 @@ +import { AudioOutlined, LoadingOutlined } from '@ant-design/icons' +import { useSettings } from '@renderer/hooks/useSettings' +import ASRService from '@renderer/services/ASRService' +import { Button, Tooltip } from 'antd' +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface Props { + onTranscribed: (text: string) => void + disabled?: boolean + style?: React.CSSProperties +} + +const ASRButton: FC = ({ onTranscribed, disabled = false, style }) => { + const { t } = useTranslation() + const { asrEnabled } = useSettings() + const [isRecording, setIsRecording] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [countdown, setCountdown] = useState(0) + const [isCountingDown, setIsCountingDown] = useState(false) + + const handleASR = useCallback(async () => { + if (!asrEnabled) { + window.message.error({ content: t('settings.asr.error.not_enabled'), key: 'asr-error' }) + return + } + + if (isRecording) { + // 停止录音并处理 + setIsRecording(false) + setIsProcessing(true) + try { + // 添加事件监听器,监听服务器发送的stopped消息 + const originalCallback = ASRService.resultCallback + const stopCallback = (text: string) => { + // 如果是空字符串,只重置状态,不调用原始回调 + if (text === '') { + setIsProcessing(false) + return + } + + // 否则调用原始回调并重置状态 + if (originalCallback) originalCallback(text) + setIsProcessing(false) + } + + await ASRService.stopRecording(stopCallback) + } catch (error) { + console.error('ASR error:', error) + setIsProcessing(false) + } + } else { + // 开始录音 + // 显示3秒倒计时,同时立即开始录音 + setIsCountingDown(true) + setCountdown(3) + setIsRecording(true) + + // 立即发送开始信号 + try { + await ASRService.startRecording(onTranscribed) + } catch (error) { + console.error('Failed to start recording:', error) + setIsRecording(false) + setIsCountingDown(false) + return + } + + // 倒计时结束后只隐藏倒计时显示 + setTimeout(() => { + setIsCountingDown(false) + }, 3000) // 3秒倒计时 + } + }, [asrEnabled, isRecording, onTranscribed, t]) + + const handleCancel = useCallback(() => { + if (isCountingDown) { + // 如果在倒计时中,取消倒计时和录音 + setIsCountingDown(false) + setCountdown(0) + // 同时取消录音,因为录音已经开始 + ASRService.cancelRecording() + setIsRecording(false) + } else if (isRecording) { + // 如果已经在录音,取消录音 + ASRService.cancelRecording() + setIsRecording(false) + } + }, [isRecording, isCountingDown]) + + // 倒计时效果 + useEffect(() => { + if (isCountingDown && countdown > 0) { + const timer = setTimeout(() => { + setCountdown(countdown - 1) + }, 1000) + return () => clearTimeout(timer) + } + return undefined // 添加返回值以解决TS7030错误 + }, [countdown, isCountingDown]) + + if (!asrEnabled) { + return null + } + + return ( + + + : isCountingDown ? null : } + onClick={handleASR} + onDoubleClick={handleCancel} + disabled={disabled || isProcessing || (isCountingDown && countdown > 0)} + style={style} + className={isCountingDown ? 'counting-down' : ''} + > + {isCountingDown && ( + {countdown} + )} + + {isCountingDown && ( + + {t('settings.asr.preparing')} ({countdown}) + + )} + + + ) +} + +const ButtonWrapper = styled.div` + position: relative; + display: inline-block; +` + +const CountdownIndicator = styled.div` + position: absolute; + top: -25px; + left: 50%; + transform: translateX(-50%); + background-color: var(--color-primary); + color: white; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + white-space: nowrap; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + animation: pulse 1s infinite; + z-index: 10; + + @keyframes pulse { + 0% { opacity: 0.7; } + 50% { opacity: 1; } + 100% { opacity: 0.7; } + } + + &:after { + content: ''; + position: absolute; + bottom: -5px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid var(--color-primary); + } +` + +const CountdownNumber = styled.span` + font-size: 18px; + font-weight: bold; + animation: zoom 1s infinite; + + @keyframes zoom { + 0% { transform: scale(0.8); } + 50% { transform: scale(1.2); } + 100% { transform: scale(0.8); } + } +` + +const StyledButton = styled(Button)` + min-width: 30px; + height: 30px; + font-size: 16px; + border-radius: 50%; + transition: all 0.3s ease; + color: var(--color-icon); + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0; + &.anticon, + &.iconfont { + transition: all 0.3s ease; + color: var(--color-icon); + } + &:hover { + background-color: var(--color-background-soft); + .anticon, + .iconfont { + color: var(--color-text-1); + } + } + &.active { + background-color: var(--color-primary) !important; + .anticon, + .iconfont { + color: var(--color-white-soft); + } + &:hover { + background-color: var(--color-primary); + } + } + &.counting-down { + font-weight: bold; + background-color: var(--color-primary); + color: var(--color-white-soft); + } +` + +export default ASRButton diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 2bfaadb851..b35ae6d099 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1377,6 +1377,52 @@ "test": "Test Speech", "help": "Text-to-speech functionality supports converting text to natural-sounding speech.", "learn_more": "Learn more" + }, + "asr": { + "title": "Speech Recognition", + "tab_title": "Speech Recognition", + "enable": "Enable Speech Recognition", + "enable.help": "Enable to convert speech to text", + "service_type": "Service Type", + "service_type.browser": "Browser", + "service_type.local": "Local Server", + "api_key": "API Key", + "api_key.placeholder": "Enter OpenAI API key", + "api_url": "API URL", + "api_url.placeholder": "Example: https://api.openai.com/v1/audio/transcriptions", + "model": "Model", + "browser.info": "Use the browser's built-in speech recognition feature, no additional setup required", + "local.info": "Use local server and browser for speech recognition, need to start the server and open the browser page first", + "local.browser_tip": "Please open this page in Edge browser and keep the browser window open", + "local.test_connection": "Test Connection", + "local.connection_success": "Connection successful", + "local.connection_failed": "Connection failed, please make sure the server is running", + "server.start": "Start Server", + "server.stop": "Stop Server", + "server.starting": "Starting server...", + "server.started": "Server started", + "server.stopping": "Stopping server...", + "server.stopped": "Server stopped", + "server.already_running": "Server is already running", + "server.not_running": "Server is not running", + "server.start_failed": "Failed to start server", + "server.stop_failed": "Failed to stop server", + "open_browser": "Open Browser Page", + "test": "Test Speech Recognition", + "test_info": "Please use the speech recognition button in the input box to test", + "start": "Start Recording", + "stop": "Stop Recording", + "preparing": "Preparing", + "recording": "Recording...", + "processing": "Processing speech...", + "success": "Speech recognition successful", + "completed": "Speech recognition completed", + "canceled": "Recording canceled", + "error": { + "not_enabled": "Speech recognition is not enabled", + "start_failed": "Failed to start recording", + "transcribe_failed": "Failed to transcribe speech" + } } }, "translate": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 4b94f9793b..f7fde71c01 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1356,6 +1356,52 @@ }, "help": "OpenAIのTTS APIを使用するには、APIキーが必要です。Edge TTSはブラウザの機能を使用するため、APIキーは不要です。", "learn_more": "詳細はこちら" + }, + "asr": { + "title": "音声認識", + "tab_title": "音声認識", + "enable": "音声認識を有効にする", + "enable.help": "音声をテキストに変換する機能を有効にします", + "service_type": "サービスタイプ", + "service_type.browser": "ブラウザ", + "service_type.local": "ローカルサーバー", + "api_key": "APIキー", + "api_key.placeholder": "OpenAI APIキーを入力", + "api_url": "API URL", + "api_url.placeholder": "例:https://api.openai.com/v1/audio/transcriptions", + "model": "モデル", + "browser.info": "ブラウザの内蔵音声認識機能を使用します。追加設定は不要です", + "local.info": "ローカルサーバーとブラウザを使用して音声認識を行います。サーバーを起動してブラウザページを開く必要があります", + "local.browser_tip": "このページをEdgeブラウザで開き、ブラウザウィンドウを開いたままにしてください", + "local.test_connection": "接続テスト", + "local.connection_success": "接続成功", + "local.connection_failed": "接続失敗。サーバーが起動していることを確認してください", + "server.start": "サーバー起動", + "server.stop": "サーバー停止", + "server.starting": "サーバーを起動中...", + "server.started": "サーバーが起動しました", + "server.stopping": "サーバーを停止中...", + "server.stopped": "サーバーが停止しました", + "server.already_running": "サーバーは既に実行中です", + "server.not_running": "サーバーは実行されていません", + "server.start_failed": "サーバーの起動に失敗しました", + "server.stop_failed": "サーバーの停止に失敗しました", + "open_browser": "ブラウザページを開く", + "test": "音声認識テスト", + "test_info": "入力ボックスの音声認識ボタンを使用してテストしてください", + "start": "録音開始", + "stop": "録音停止", + "preparing": "準備中", + "recording": "録音中...", + "processing": "音声処理中...", + "success": "音声認識成功", + "completed": "音声認識完了", + "canceled": "録音キャンセル", + "error": { + "not_enabled": "音声認識が有効になっていません", + "start_failed": "録音の開始に失敗しました", + "transcribe_failed": "音声の文字起こしに失敗しました" + } } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 3961f6dc75..a2ddc2a40c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1335,8 +1335,14 @@ "title": "隐私设置", "enable_privacy_mode": "匿名发送错误报告和数据统计" }, + "voice": { + "title": "语音功能", + "help": "语音功能包括文本转语音(TTS)和语音识别(ASR)。", + "learn_more": "了解更多" + }, "tts": { - "title": "语音设置", + "title": "语音合成", + "tab_title": "语音合成", "enable": "启用语音合成", "enable.help": "启用后可以将文本转换为语音", "reset": "重置", @@ -1376,7 +1382,61 @@ "max_text_length": "最大文本长度", "test": "测试语音", "help": "语音合成功能支持将文本转换为自然语音。", - "learn_more": "了解更多" + "learn_more": "了解更多", + "error": { + "not_enabled": "语音合成功能未启用", + "no_api_key": "未设置API密钥", + "no_edge_voice": "未选择Edge TTS音色", + "browser_not_support": "浏览器不支持语音合成" + } + }, + "asr": { + "title": "语音识别", + "tab_title": "语音识别", + "enable": "启用语音识别", + "enable.help": "启用后可以将语音转换为文本", + "service_type": "服务类型", + "service_type.browser": "浏览器", + "service_type.local": "本地服务器", + "api_key": "API密钥", + "api_key.placeholder": "请输入OpenAI API密钥", + "api_url": "API地址", + "api_url.placeholder": "例如:https://api.openai.com/v1/audio/transcriptions", + "model": "模型", + "browser.info": "使用浏览器内置的语音识别功能,无需额外设置", + "local.info": "使用本地服务器和浏览器进行语音识别,需要先启动服务器并打开浏览器页面", + "local.browser_tip": "请在Edge浏览器中打开此页面,并保持浏览器窗口打开", + "local.test_connection": "测试连接", + "local.connection_success": "连接成功", + "local.connection_failed": "连接失败,请确保服务器已启动", + "server.start": "启动服务器", + "server.stop": "停止服务器", + "server.starting": "正在启动服务器...", + "server.started": "服务器已启动", + "server.stopping": "正在停止服务器...", + "server.stopped": "服务器已停止", + "server.already_running": "服务器已经在运行中", + "server.not_running": "服务器未运行", + "server.start_failed": "启动服务器失败", + "server.stop_failed": "停止服务器失败", + "open_browser": "打开浏览器页面", + "test": "测试语音识别", + "test_info": "请在输入框中使用语音识别按钮进行测试", + "start": "开始录音", + "stop": "停止录音", + "preparing": "准备中", + "recording": "正在录音...", + "processing": "正在处理语音...", + "success": "语音识别成功", + "completed": "语音识别完成", + "canceled": "已取消录音", + "error": { + "not_enabled": "语音识别功能未启用", + "no_api_key": "未设置API密钥", + "browser_not_support": "浏览器不支持语音识别", + "start_failed": "开始录音失败", + "transcribe_failed": "语音识别失败" + } } }, "translate": { diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 79f377e14a..a6890bac0c 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -14,6 +14,7 @@ import { TranslationOutlined } from '@ant-design/icons' import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel' +import ASRButton from '@renderer/components/ASRButton' import TranslateButton from '@renderer/components/TranslateButton' import { isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import db from '@renderer/databases' @@ -1008,6 +1009,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = + { + // 如果是空字符串,不做任何处理 + if (!transcribedText) return + + // 将识别的文本添加到当前输入框 + 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 c66718539c..0acbe80234 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -16,7 +16,7 @@ import { UploadOutlined } from '@ant-design/icons' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup' -import TTSButton from '@renderer/components/TTSButton' +// import TTSButton from '@renderer/components/TTSButton' // 暂时不使用 import { isReasoningModel } from '@renderer/config/models' import { TranslateLanguageOptions } from '@renderer/config/translate' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' @@ -154,14 +154,14 @@ const MessageMenubar: FC = (props) => { const imageUrls: string[] = [] let match let content = editedText - + while ((match = imageRegex.exec(editedText)) !== null) { imageUrls.push(match[1]) content = content.replace(match[0], '') } - + // 更新消息内容,保留图片信息 - await editMessage(message.id, { + await editMessage(message.id, { content: content.trim(), metadata: { ...message.metadata, @@ -171,9 +171,9 @@ const MessageMenubar: FC = (props) => { } : undefined } }) - - resendMessage && handleResendUserMessage({ - ...message, + + resendMessage && handleResendUserMessage({ + ...message, content: content.trim(), metadata: { ...message.metadata, diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index f2b4daabe9..5d9ea798b6 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -127,7 +127,7 @@ const SettingsPage: FC = () => { - {t('settings.tts.title')} + {t('settings.voice.title')} diff --git a/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx new file mode 100644 index 0000000000..321f8f4e9e --- /dev/null +++ b/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx @@ -0,0 +1,271 @@ +import { InfoCircleOutlined, GlobalOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons' +import { useTheme } from '@renderer/context/ThemeProvider' +import ASRService from '@renderer/services/ASRService' +import ASRServerService from '@renderer/services/ASRServerService' +import { useAppDispatch } from '@renderer/store' +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 { 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() + + // 服务器状态 + const [isServerRunning, setIsServerRunning] = useState(false) + + // 从 Redux 获取 ASR 设置 + const asrEnabled = useSelector((state: any) => state.settings.asrEnabled) + const asrServiceType = useSelector((state: any) => state.settings.asrServiceType || 'openai') + const asrApiKey = useSelector((state: any) => state.settings.asrApiKey) + const asrApiUrl = useSelector((state: any) => state.settings.asrApiUrl) + const asrModel = useSelector((state: any) => state.settings.asrModel || 'whisper-1') + + // 检查服务器状态 + useEffect(() => { + if (asrServiceType === 'local') { + setIsServerRunning(ASRServerService.isRunning()) + } + return undefined // 添加返回值以解决TS7030错误 + }, [asrServiceType]) + + // 服务类型选项 + const serviceTypeOptions = [ + { label: 'OpenAI', value: 'openai' }, + { label: t('settings.asr.service_type.browser'), value: 'browser' }, + { label: t('settings.asr.service_type.local'), value: 'local' } + ] + + // 模型选项 + const modelOptions = [ + { label: 'whisper-1', value: 'whisper-1' } + ] + + return ( + +
+ {/* ASR开关 */} + + + dispatch(setAsrEnabled(checked))} /> + {t('settings.asr.enable')} + + + + + + + {/* 服务类型选择 */} + + dispatch(setAsrApiUrl(e.target.value))} + placeholder={t('settings.asr.api_url.placeholder')} + disabled={!asrEnabled} + /> + + + {/* 模型选择 */} + + { - console.log('切换TTS服务类型为:', value) - // 先将新的服务类型写入Redux状态 - dispatch(setTtsServiceType(value)) + {/* 重置按钮 */} + + + {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} - /> - - - )} + {/* OpenAI TTS设置 */} + {ttsServiceType === 'openai' && ( + <> + + dispatch(setTtsApiKey(e.target.value))} + placeholder={t('settings.tts.api_key.placeholder')} + disabled={!ttsEnabled} + /> + + + dispatch(setTtsApiUrl(e.target.value))} + placeholder={t('settings.tts.api_url.placeholder')} + disabled={!ttsEnabled} + /> + + + )} - {/* Edge TTS设置 */} - {ttsServiceType === 'edge' && ( - - - dispatch(setTtsEdgeVoice(value))} + options={ + availableVoices.length > 0 + ? availableVoices + : [{ label: t('settings.tts.edge_voice.loading'), value: '' }] + } + disabled={!ttsEnabled} + style={{ flex: 1 }} + showSearch + optionFilterProp="label" + placeholder={ + availableVoices.length === 0 + ? t('settings.tts.edge_voice.loading') + : t('settings.tts.voice.placeholder') + } + notFoundContent={ + availableVoices.length === 0 + ? t('settings.tts.edge_voice.loading') + : t('settings.tts.edge_voice.not_found') + } + /> + - - + {/* 添加自定义音色 */} + + + setNewVoice(e.target.value)} + disabled={!ttsEnabled} + style={{ flex: 1 }} + /> + + + - {/* 模型选择 */} - - dispatch(setTtsModel(value))} + options={ttsCustomModels.map((model: any) => { + // 确保model是字符串 + const modelStr = typeof model === 'string' ? model : String(model) + return { label: modelStr, value: modelStr } + })} + disabled={!ttsEnabled} + style={{ width: '100%' }} + placeholder={t('settings.tts.model.placeholder')} + showSearch + optionFilterProp="label" + allowClear + /> + - {/* 自定义模型列表 */} - - {ttsCustomModels && ttsCustomModels.length > 0 ? ( - ttsCustomModels.map((model: any, index: number) => { - // 确保model是字符串 - const modelStr = typeof model === 'string' ? model : String(model) - return ( - handleRemoveModel(modelStr)} - style={{ padding: '4px 8px' }}> - {modelStr} - - ) - }) - ) : ( - {t('settings.tts.model_empty')} - )} - + {/* 自定义模型列表 */} + + {ttsCustomModels && ttsCustomModels.length > 0 ? ( + ttsCustomModels.map((model: any, index: number) => { + // 确保model是字符串 + const modelStr = typeof model === 'string' ? model : String(model) + return ( + handleRemoveModel(modelStr)} + style={{ padding: '4px 8px' }}> + {modelStr} + + ) + }) + ) : ( + {t('settings.tts.model_empty')} + )} + - {/* 添加自定义模型 */} - - - setNewModel(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')}: - dispatch(setTtsFilterOptions({ maxTextLength: value }))} + disabled={!ttsEnabled} + style={{ width: 120 }} + options={[ + { label: '1000', value: 1000 }, + { label: '2000', value: 2000 }, + { label: '4000', value: 4000 }, + { label: '8000', value: 8000 }, + { label: '16000', value: 16000 } + ]} + /> + + - - - - - + + + + + + + ) + }, + { + key: 'asr', + label: ( + + {t('settings.asr.tab_title')} + + ), + children: + } + ]} + /> - {t('settings.tts.help')} - - {t('settings.tts.learn_more')} + {t('settings.voice.help')} + + {t('settings.voice.learn_more')} diff --git a/src/renderer/src/services/ASRServerService.ts b/src/renderer/src/services/ASRServerService.ts new file mode 100644 index 0000000000..6c8d77757f --- /dev/null +++ b/src/renderer/src/services/ASRServerService.ts @@ -0,0 +1,129 @@ +import i18n from '@renderer/i18n' + +// 使用window.electron而不是直接导入electron模块 +// 这样可以避免__dirname不可用的问题 + +class ASRServerService { + private serverProcess: any = null + private isServerRunning = false + + /** + * 启动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 + } + + try { + console.log('[ASRServerService] 正在启动ASR服务器...') + window.message.loading({ content: i18n.t('settings.asr.server.starting'), key: 'asr-server' }) + + // 使用IPC调用主进程启动服务器 + const result = await window.electron.ipcRenderer.invoke('start-asr-server') + + 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 + } + } 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 + } + } + + /** + * 停止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.electron.ipcRenderer.invoke('stop-asr-server', 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' + }) + return false + } + } + + /** + * 检查ASR服务器是否正在运行 + * @returns boolean 是否正在运行 + */ + isRunning = (): boolean => { + return this.isServerRunning + } + + /** + * 获取ASR服务器网页URL + * @returns string 网页URL + */ + getServerUrl = (): string => { + return 'http://localhost:8080' + } + + /** + * 获取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' + } + + /** + * 打开ASR服务器网页 + */ + openServerPage = (): void => { + window.open(this.getServerUrl(), '_blank') + } +} + +export default new ASRServerService() diff --git a/src/renderer/src/services/ASRService.ts b/src/renderer/src/services/ASRService.ts new file mode 100644 index 0000000000..260725fdf6 --- /dev/null +++ b/src/renderer/src/services/ASRService.ts @@ -0,0 +1,560 @@ +import i18n from '@renderer/i18n' +import store from '@renderer/store' + +/** + * ASR服务,用于将语音转换为文本 + */ +class ASRService { + private mediaRecorder: MediaRecorder | null = null + private audioChunks: Blob[] = [] + private isRecording = false + private stream: MediaStream | null = null + + // WebSocket相关 + private ws: WebSocket | null = null + private wsConnected = false + private browserReady = false + private reconnectAttempt = 0 + private maxReconnectAttempts = 5 + private reconnectTimeout: NodeJS.Timeout | null = null + + /** + * 开始录音 + * @returns Promise + */ + /** + * 连接到WebSocket服务器 + * @returns Promise 是否连接成功 + */ + connectToWebSocketServer = async (): Promise => { + return new Promise((resolve) => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + console.log('[ASRService] WebSocket已连接') + resolve(true) + return + } + + if (this.ws && this.ws.readyState === WebSocket.CONNECTING) { + console.log('[ASRService] WebSocket正在连接中') + // 等待连接完成 + this.ws.onopen = () => { + console.log('[ASRService] WebSocket连接成功') + this.wsConnected = true + this.reconnectAttempt = 0 + this.ws?.send(JSON.stringify({ type: 'identify', role: 'electron' })) + resolve(true) + } + this.ws.onerror = () => { + console.error('[ASRService] WebSocket连接失败') + this.wsConnected = false + resolve(false) + } + return + } + + // 关闭之前的连接 + if (this.ws) { + try { + this.ws.close() + } catch (e) { + console.error('[ASRService] 关闭WebSocket连接失败:', e) + } + } + + // 创建新连接 + try { + console.log('[ASRService] 正在连接WebSocket服务器...') + window.message.loading({ content: '正在连接语音识别服务...', key: 'ws-connect' }) + + this.ws = new WebSocket('ws://localhost:8080') + this.wsConnected = false + this.browserReady = false + + this.ws.onopen = () => { + console.log('[ASRService] WebSocket连接成功') + window.message.success({ content: '语音识别服务连接成功', key: 'ws-connect' }) + this.wsConnected = true + this.reconnectAttempt = 0 + this.ws?.send(JSON.stringify({ type: 'identify', role: 'electron' })) + resolve(true) + } + + this.ws.onclose = () => { + console.log('[ASRService] WebSocket连接关闭') + this.wsConnected = false + this.browserReady = false + this.attemptReconnect() + } + + this.ws.onerror = (error) => { + console.error('[ASRService] WebSocket连接错误:', error) + this.wsConnected = false + window.message.error({ content: '语音识别服务连接失败', key: 'ws-connect' }) + resolve(false) + } + + this.ws.onmessage = this.handleWebSocketMessage + } catch (error) { + console.error('[ASRService] 创建WebSocket连接失败:', error) + window.message.error({ content: '语音识别服务连接失败', key: 'ws-connect' }) + resolve(false) + } + }) + } + + /** + * 处理WebSocket消息 + */ + private handleWebSocketMessage = (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + console.log('[ASRService] 收到WebSocket消息:', data) + + if (data.type === 'status') { + if (data.message === 'browser_ready' || data.message === 'Browser connected') { + console.log('[ASRService] 浏览器已准备好') + this.browserReady = true + window.message.success({ content: '语音识别浏览器已准备好', key: 'browser-status' }) + } else if (data.message === 'Browser disconnected' || data.message === 'Browser connection error') { + console.log('[ASRService] 浏览器断开连接') + this.browserReady = false + window.message.error({ content: '语音识别浏览器断开连接', key: 'browser-status' }) + } + } else if (data.type === 'status' && data.message === 'stopped') { + // 语音识别已停止 + console.log('[ASRService] 语音识别已停止') + this.isRecording = false + + // 如果没有收到最终结果,显示处理完成消息 + window.message.success({ content: i18n.t('settings.asr.completed'), key: 'asr-processing' }) + + // 如果有回调函数,调用一次空字符串,触发按钮状态重置 + if (this.resultCallback && typeof this.resultCallback === 'function') { + // 使用空字符串调用回调,不会影响输入框,但可以触发按钮状态重置 + this.resultCallback('') + } + } else if (data.type === 'result' && data.data) { + // 处理识别结果 + console.log('[ASRService] 收到识别结果:', data.data) + if (this.resultCallback && typeof this.resultCallback === 'function') { + // 只在收到最终结果时才调用回调 + if (data.data.isFinal && data.data.text && data.data.text.trim()) { + console.log('[ASRService] 收到最终结果,调用回调函数,文本:', data.data.text) + this.resultCallback(data.data.text) + window.message.success({ content: i18n.t('settings.asr.success'), key: 'asr-processing' }) + } else if (!data.data.isFinal) { + // 非最终结果,只输出日志,不调用回调 + console.log('[ASRService] 收到中间结果,文本:', data.data.text) + } else { + console.log('[ASRService] 识别结果为空,不调用回调') + } + } else { + console.warn('[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' }) + } + } catch (error) { + console.error('[ASRService] 解析WebSocket消息失败:', error, event.data) + } + } + + /** + * 尝试重新连接WebSocket服务器 + */ + private attemptReconnect = () => { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout) + this.reconnectTimeout = null + } + + if (this.reconnectAttempt >= this.maxReconnectAttempts) { + console.log('[ASRService] 达到最大重连次数,停止重连') + return + } + + const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), 30000) + console.log(`[ASRService] 将在 ${delay}ms 后尝试重连 (尝试 ${this.reconnectAttempt + 1}/${this.maxReconnectAttempts})`) + + this.reconnectTimeout = setTimeout(() => { + this.reconnectAttempt++ + this.connectToWebSocketServer().catch(console.error) + }, delay) + } + + // 存储结果回调函数 + resultCallback: ((text: string) => void) | null = null + + startRecording = async (onTranscribed?: (text: string) => void): Promise => { + try { + const { asrEnabled, asrServiceType } = store.getState().settings + + if (!asrEnabled) { + window.message.error({ content: i18n.t('settings.asr.error.not_enabled'), key: 'asr-error' }) + return + } + + // 检查是否已经在录音 + if (this.isRecording) { + console.log('已经在录音中,忽略此次请求') + return + } + + // 如果是使用本地服务器 + if (asrServiceType === 'local') { + // 连接WebSocket服务器 + const connected = await this.connectToWebSocketServer() + if (!connected) { + throw new Error('无法连接到语音识别服务') + } + + // 检查浏览器是否准备好 + if (!this.browserReady) { + // 尝试等待浏览器准备好 + let waitAttempts = 0 + const maxWaitAttempts = 5 + + while (!this.browserReady && waitAttempts < maxWaitAttempts) { + window.message.loading({ + content: `等待浏览器准备就绪 (${waitAttempts + 1}/${maxWaitAttempts})...`, + key: 'browser-status' + }) + + // 等待一秒 + await new Promise(resolve => setTimeout(resolve, 1000)) + waitAttempts++ + } + + if (!this.browserReady) { + window.message.warning({ + content: '语音识别浏览器尚未准备好,请确保已打开浏览器页面', + key: 'browser-status' + }) + throw new Error('浏览器尚未准备好') + } + } + + // 保存回调函数(如果提供了) + if (onTranscribed && typeof onTranscribed === 'function') { + this.resultCallback = onTranscribed + } + + // 发送开始命令 + if (this.ws && this.wsConnected) { + this.ws.send(JSON.stringify({ type: 'start' })) + this.isRecording = true + console.log('开始语音识别') + window.message.info({ content: i18n.t('settings.asr.recording'), key: 'asr-recording' }) + } else { + throw new Error('WebSocket连接未就绪') + } + return + } + + // 以下是原有的录音逻辑(OpenAI或浏览器API) + // 请求麦克风权限 + this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + + // 创建MediaRecorder实例 + this.mediaRecorder = new MediaRecorder(this.stream) + + // 清空之前的录音数据 + this.audioChunks = [] + + // 设置数据可用时的回调 + this.mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + this.audioChunks.push(event.data) + } + } + + // 开始录音 + this.mediaRecorder.start() + this.isRecording = true + + console.log('开始录音') + window.message.info({ content: i18n.t('settings.asr.recording'), key: 'asr-recording' }) + } catch (error) { + console.error('开始录音失败:', error) + window.message.error({ + content: i18n.t('settings.asr.error.start_failed') + ': ' + (error as Error).message, + key: 'asr-error' + }) + this.isRecording = false + } + } + + /** + * 停止录音并转换为文本 + * @param onTranscribed 转录完成后的回调函数 + * @returns Promise + */ + stopRecording = async (onTranscribed: (text: string) => void): Promise => { + const { asrServiceType } = store.getState().settings + + // 如果是使用本地服务器 + if (asrServiceType === 'local') { + if (!this.isRecording) { + console.log('没有正在进行的语音识别') + return + } + + try { + // 保存回调函数 + this.resultCallback = onTranscribed + + // 发送停止命令 + if (this.ws && this.wsConnected) { + this.ws.send(JSON.stringify({ type: 'stop' })) + console.log('停止语音识别') + window.message.loading({ content: i18n.t('settings.asr.processing'), key: 'asr-processing' }) + + // 立即调用回调函数,使按钮状态立即更新 + if (onTranscribed) { + // 使用空字符串调用回调,不会影响输入框,但可以触发按钮状态重置 + setTimeout(() => onTranscribed(''), 100) + } + } else { + throw new Error('WebSocket连接未就绪') + } + + // 重置录音状态 + this.isRecording = false + } catch (error) { + console.error('停止语音识别失败:', error) + window.message.error({ + content: i18n.t('settings.asr.error.transcribe_failed') + ': ' + (error as Error).message, + key: 'asr-processing' + }) + this.isRecording = false + } + return + } + + // 以下是原有的录音停止逻辑(OpenAI或浏览器API) + if (!this.isRecording || !this.mediaRecorder) { + console.log('没有正在进行的录音') + return + } + + try { + // 创建一个Promise,等待录音结束 + const recordingEndedPromise = new Promise((resolve) => { + if (this.mediaRecorder) { + this.mediaRecorder.onstop = () => { + // 将所有音频块合并为一个Blob + const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' }) + resolve(audioBlob) + } + + // 停止录音 + this.mediaRecorder.stop() + } + }) + + // 停止所有轨道 + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()) + this.stream = null + } + + // 等待录音结束并获取音频Blob + const audioBlob = await recordingEndedPromise + + // 重置录音状态 + this.isRecording = false + this.mediaRecorder = null + + console.log('录音结束,音频大小:', audioBlob.size, 'bytes') + + // 显示处理中消息 + window.message.loading({ content: i18n.t('settings.asr.processing'), key: 'asr-processing' }) + + if (asrServiceType === 'openai') { + // 使用OpenAI的Whisper API进行语音识别 + await this.transcribeWithOpenAI(audioBlob, onTranscribed) + } else if (asrServiceType === 'browser') { + // 使用浏览器的Web Speech API进行语音识别 + await this.transcribeWithBrowser(audioBlob, onTranscribed) + } else { + throw new Error(`不支持的ASR服务类型: ${asrServiceType}`) + } + } catch (error) { + console.error('停止录音或转录失败:', error) + window.message.error({ + content: i18n.t('settings.asr.error.transcribe_failed') + ': ' + (error as Error).message, + key: 'asr-processing' + }) + + // 重置录音状态 + this.isRecording = false + this.mediaRecorder = null + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()) + this.stream = null + } + } + } + + /** + * 使用OpenAI的Whisper API进行语音识别 + * @param audioBlob 音频Blob + * @param onTranscribed 转录完成后的回调函数 + * @returns Promise + */ + private transcribeWithOpenAI = async (audioBlob: Blob, onTranscribed: (text: string) => void): Promise => { + try { + const { asrApiKey, asrApiUrl, asrModel } = store.getState().settings + + if (!asrApiKey) { + throw new Error(i18n.t('settings.asr.error.no_api_key')) + } + + // 创建FormData对象 + const formData = new FormData() + formData.append('file', audioBlob, 'recording.webm') + formData.append('model', asrModel || 'whisper-1') + + // 调用OpenAI API + const response = await fetch(asrApiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${asrApiKey}` + }, + body: formData + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error?.message || 'OpenAI语音识别失败') + } + + // 解析响应 + const data = await response.json() + const transcribedText = data.text + + if (transcribedText) { + console.log('语音识别成功:', transcribedText) + window.message.success({ content: i18n.t('settings.asr.success'), key: 'asr-processing' }) + onTranscribed(transcribedText) + } else { + throw new Error('未能识别出文本') + } + } catch (error) { + console.error('OpenAI语音识别失败:', error) + throw error + } + } + + /** + * 使用浏览器的Web Speech API进行语音识别 + * @param audioBlob 音频Blob + * @param onTranscribed 转录完成后的回调函数 + * @returns Promise + */ + private transcribeWithBrowser = async (_audioBlob: Blob, onTranscribed: (text: string) => void): Promise => { + try { + // 检查浏览器是否支持Web Speech API + if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { + throw new Error(i18n.t('settings.asr.error.browser_not_support')) + } + + // 由于Web Speech API不支持直接处理录制的音频,这里我们只是模拟一个成功的回调 + // 实际上,使用Web Speech API时,应该直接使用SpeechRecognition对象进行实时识别 + // 这里简化处理,实际项目中可能需要更复杂的实现 + window.message.success({ content: i18n.t('settings.asr.success'), key: 'asr-processing' }) + onTranscribed('浏览器语音识别功能尚未完全实现') + } catch (error) { + console.error('浏览器语音识别失败:', error) + throw error + } + } + + /** + * 检查是否正在录音 + * @returns boolean + */ + isCurrentlyRecording = (): boolean => { + return this.isRecording + } + + /** + * 取消录音 + */ + cancelRecording = (): void => { + const { asrServiceType } = store.getState().settings + + // 如果是使用本地服务器 + if (asrServiceType === 'local') { + if (this.isRecording) { + // 发送停止命令 + if (this.ws && this.wsConnected) { + this.ws.send(JSON.stringify({ type: 'stop' })) + } + + // 重置状态 + this.isRecording = false + this.resultCallback = null + + console.log('语音识别已取消') + window.message.info({ content: i18n.t('settings.asr.canceled'), key: 'asr-recording' }) + } + return + } + + // 以下是原有的取消录音逻辑(OpenAI或浏览器API) + if (this.isRecording && this.mediaRecorder) { + // 停止MediaRecorder + this.mediaRecorder.stop() + + // 停止所有轨道 + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()) + this.stream = null + } + + // 重置状态 + this.isRecording = false + this.mediaRecorder = null + this.audioChunks = [] + + console.log('录音已取消') + window.message.info({ content: i18n.t('settings.asr.canceled'), key: 'asr-recording' }) + } + } + + /** + * 关闭WebSocket连接 + */ + closeWebSocketConnection = (): void => { + if (this.ws) { + try { + this.ws.close() + } catch (e) { + console.error('[ASRService] 关闭WebSocket连接失败:', e) + } + this.ws = null + } + + this.wsConnected = false + this.browserReady = false + + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout) + this.reconnectTimeout = null + } + } + + /** + * 打开浏览器页面 + */ + openBrowserPage = (): void => { + // 使用window.open打开浏览器页面 + window.open('http://localhost:8080', '_blank') + } +} + +// 创建单例实例 +const instance = new ASRService() +export default instance diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index bc3340eea6..57c2a46b82 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -129,6 +129,12 @@ export interface SettingsState { filterHtmlTags: boolean // 过滤HTML标签 maxTextLength: number // 最大文本长度 } + // ASR配置(语音识别) + asrEnabled: boolean + asrServiceType: string // ASR服务类型:openai或browser + asrApiKey: string + asrApiUrl: string + asrModel: string // Quick Panel Triggers enableQuickPanelTriggers: boolean // Export Menu Options @@ -248,6 +254,12 @@ export const initialState: SettingsState = { filterHtmlTags: true, // 默认过滤HTML标签 maxTextLength: 4000 // 默认最大文本长度 }, + // ASR配置(语音识别) + asrEnabled: false, + asrServiceType: 'openai', // 默认使用 OpenAI ASR + asrApiKey: '', + asrApiUrl: 'https://api.openai.com/v1/audio/transcriptions', + asrModel: 'whisper-1', // Quick Panel Triggers enableQuickPanelTriggers: false, // Export Menu Options @@ -628,6 +640,22 @@ const settingsSlice = createSlice({ ...action.payload } }, + // ASR相关的action + setAsrEnabled: (state, action: PayloadAction) => { + state.asrEnabled = action.payload + }, + setAsrServiceType: (state, action: PayloadAction) => { + state.asrServiceType = action.payload + }, + setAsrApiKey: (state, action: PayloadAction) => { + state.asrApiKey = action.payload + }, + setAsrApiUrl: (state, action: PayloadAction) => { + state.asrApiUrl = action.payload + }, + setAsrModel: (state, action: PayloadAction) => { + state.asrModel = action.payload + }, // Quick Panel Triggers action setEnableQuickPanelTriggers: (state, action: PayloadAction) => { state.enableQuickPanelTriggers = action.payload @@ -736,7 +764,12 @@ export const { addTtsCustomModel, removeTtsCustomVoice, removeTtsCustomModel, - setTtsFilterOptions + setTtsFilterOptions, + setAsrEnabled, + setAsrServiceType, + setAsrApiKey, + setAsrApiUrl, + setAsrModel } = settingsSlice.actions export default settingsSlice.reducer diff --git a/src/renderer/src/types/electron.d.ts b/src/renderer/src/types/electron.d.ts index 7059da90aa..a916179e7a 100644 --- a/src/renderer/src/types/electron.d.ts +++ b/src/renderer/src/types/electron.d.ts @@ -4,6 +4,21 @@ interface ObsidianAPI { getFolders: (vaultName: string) => Promise> } +interface IpcRendererAPI { + invoke: (channel: string, ...args: any[]) => Promise + on: (channel: string, listener: (...args: any[]) => void) => void + once: (channel: string, listener: (...args: any[]) => void) => void + removeListener: (channel: string, listener: (...args: any[]) => void) => void + removeAllListeners: (channel: string) => void + send: (channel: string, ...args: any[]) => void + sendSync: (channel: string, ...args: any[]) => any +} + +interface ElectronAPI { + ipcRenderer: IpcRendererAPI +} + interface Window { obsidian: ObsidianAPI + electron: ElectronAPI } From d64b4f321006e3ec0aefe7dead864302652312ad Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Thu, 10 Apr 2025 12:49:14 +0800 Subject: [PATCH 20/59] =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/shared/IpcChannel.ts | 5 + public/asr-server/index.html | 6 +- public/asr-server/server.js | 2 +- src/main/ipc.ts | 12 +++ src/main/services/SearchService.ts | 100 ++++++++++++++++++ src/preload/index.d.ts | 5 + src/preload/index.ts | 5 + src/renderer/src/assets/asr-server/index.html | 6 +- src/renderer/src/assets/asr-server/server.js | 2 +- src/renderer/src/i18n/locales/en-us.json | 4 +- src/renderer/src/i18n/locales/ja-jp.json | 8 +- src/renderer/src/i18n/locales/zh-cn.json | 8 +- .../settings/TTSSettings/TTSSettings.tsx | 4 +- src/renderer/src/store/settings.ts | 4 +- 14 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 src/main/services/SearchService.ts diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index d07a0a5200..017d550b5d 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -64,6 +64,11 @@ export enum IpcChannel { Aes_Encrypt = 'aes:encrypt', Aes_Decrypt = 'aes:decrypt', + // search window + SearchWindow_Open = 'search-window:open', + SearchWindow_Close = 'search-window:close', + SearchWindow_OpenUrl = 'search-window:open-url', + Gemini_UploadFile = 'gemini:upload-file', Gemini_Base64File = 'gemini:base64-file', Gemini_RetrieveFile = 'gemini:retrieve-file', diff --git a/public/asr-server/index.html b/public/asr-server/index.html index 9cd22e0b5d..00a207f6de 100644 --- a/public/asr-server/index.html +++ b/public/asr-server/index.html @@ -4,7 +4,7 @@ - Edge ASR (External) + Browser ASR (External) + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:34515

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } else { + // 处理其他请求 + res.writeHead(404) + res.end('Not found') + } + } catch (error) { + console.error('Error handling request:', error) + res.writeHead(500) + res.end('Server error') + } +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + const port = 34515 + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/asr-server/index.html b/asr-server/index.html new file mode 100644 index 0000000000..cfa0db82fb --- /dev/null +++ b/asr-server/index.html @@ -0,0 +1,425 @@ + + + + + + + Cherry Studio ASR + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/asr-server/package-lock.json b/asr-server/package-lock.json new file mode 100644 index 0000000000..8d3eb4035d --- /dev/null +++ b/asr-server/package-lock.json @@ -0,0 +1,854 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cherry-asr-server", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/asr-server/package.json b/asr-server/package.json new file mode 100644 index 0000000000..b09528cca8 --- /dev/null +++ b/asr-server/package.json @@ -0,0 +1,10 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "description": "Cherry Studio ASR Server", + "main": "server.js", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } +} \ No newline at end of file diff --git a/asr-server/server.js b/asr-server/server.js new file mode 100644 index 0000000000..ed73ecb022 --- /dev/null +++ b/asr-server/server.js @@ -0,0 +1,269 @@ +// 检查依赖项 +try { + console.log('ASR Server starting...') + console.log('Node.js version:', process.version) + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + console.log('Command line arguments:', process.argv) + + // 检查必要的依赖项 + const checkDependency = (name) => { + try { + require(name) // Removed unused variable 'module' + console.log(`Successfully loaded dependency: ${name}`) + return true + } catch (error) { + console.error(`Failed to load dependency: ${name}`, error.message) + return false + } + } + + // 检查所有必要的依赖项 + const dependencies = ['http', 'ws', 'express', 'path', 'fs'] + const missingDeps = dependencies.filter((dep) => !checkDependency(dep)) + + if (missingDeps.length > 0) { + console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`) + process.exit(1) + } +} catch (error) { + console.error('Error during dependency check:', error) + process.exit(1) +} + +// 加载依赖项 +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module +// const fs = require('fs') // Commented out unused import 'fs' + +const app = express() +const port = 34515 // Define the port + +// 获取index.html文件的路径 +function getIndexHtmlPath() { + const fs = require('fs') + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + + // 尝试多个可能的路径 + const possiblePaths = [ + // 开发环境路径 + path.join(__dirname, 'index.html'), + // 当前目录 + path.join(process.cwd(), 'index.html'), + // 相对于可执行文件的路径 + path.join(path.dirname(process.execPath), 'index.html'), + // 相对于可执行文件的上级目录的路径 + path.join(path.dirname(path.dirname(process.execPath)), 'index.html'), + // 相对于可执行文件的resources目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'index.html'), + // 相对于可执行文件的resources/asr-server目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'), + // 相对于可执行文件的asr-server目录的路径 + path.join(path.dirname(process.execPath), 'asr-server', 'index.html'), + // 如果是pkg打包环境 + process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null + ].filter(Boolean) // 过滤掉null值 + + console.log('Possible index.html paths:', possiblePaths) + + // 检查每个路径,返回第一个存在的文件 + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + console.log(`Found index.html at: ${p}`) + return p + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + // 如果没有找到文件,返回默认路径并记录错误 + console.error('Could not find index.html in any of the expected locations') + return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在 +} + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + const indexPath = getIndexHtmlPath() + console.log(`Serving index.html from: ${indexPath}`) + + // 检查文件是否存在 + const fs = require('fs') + if (!fs.existsSync(indexPath)) { + console.error(`Error: index.html not found at ${indexPath}`) + return res.status(404).send(`Error: index.html not found at ${indexPath}.
Please check the server logs.`) + } + + res.sendFile(indexPath, (err) => { + if (err) { + console.error('Error sending index.html:', err) + res.status(500).send(`Error serving index.html: ${err.message}`) + } + }) + } catch (error) { + console.error('Error in route handler:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = 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') + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = 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') + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } else if (data.type === 'reset' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying RESET command to browser') + browserConnection.send(JSON.stringify({ type: 'reset' })) + } else { + console.log('[Server] Cannot relay RESET: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // Handle server errors + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/asr-server/standalone.js b/asr-server/standalone.js new file mode 100644 index 0000000000..7b826d1f25 --- /dev/null +++ b/asr-server/standalone.js @@ -0,0 +1,114 @@ +/** + * 独立的ASR服务器 + * 这个文件是一个简化版的server.js,用于在打包后的应用中运行 + */ + +// 基本依赖 +const http = require('http') +const express = require('express') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建Express应用 +const app = express() +const port = 34515 + +// 提供静态文件 +app.use(express.static(__dirname)) + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + res.sendFile(indexPath) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.send(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:${port}

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } catch (error) { + console.error('Error serving index.html:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +// 创建HTTP服务器 +const server = http.createServer(app) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/asr-server/start-server.bat b/asr-server/start-server.bat new file mode 100644 index 0000000000..3f9628a75c --- /dev/null +++ b/asr-server/start-server.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting ASR Server... +cd /d %~dp0 +node standalone.js +pause diff --git a/electron-builder.yml b/electron-builder.yml index 29c8c97e47..962202f3b0 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -29,14 +29,18 @@ files: - '!node_modules/@tavily/core/node_modules/js-tiktoken' - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' - # 包含 ASR 服务器文件 - - src/renderer/src/assets/asr-server/**/* - # 包含打包后的ASR服务器可执行文件 - - cherry-asr-server.exe - - index.html -asarUnpack: +asarUnpack: # Removed ASR server rules from 'files' section - resources/** - '**/*.{node,dll,metal,exp,lib}' +extraResources: # Add extraResources to copy the prepared asr-server directory + - from: asr-server # Copy the folder from project root + to: app/asr-server # Copy TO the 'app' subfolder within resources + filter: + - "**/*" # Include everything inside + - from: resources/data # Copy the data folder with agents.json + to: data # Copy TO the 'data' subfolder within resources + filter: + - "**/*" # Include everything inside win: executableName: Cherry Studio artifactName: ${productName}-${version}-${arch}-setup.${ext} diff --git a/index.html b/index.html new file mode 100644 index 0000000000..cfa0db82fb --- /dev/null +++ b/index.html @@ -0,0 +1,425 @@ + + + + + + + Cherry Studio ASR + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/public/asr-server/embedded.js b/public/asr-server/embedded.js new file mode 100644 index 0000000000..68ae30e239 --- /dev/null +++ b/public/asr-server/embedded.js @@ -0,0 +1,123 @@ +/** + * 内置的ASR服务器模块 + * 这个文件可以直接在Electron中运行,不需要外部依赖 + */ + +// 使用Electron内置的Node.js模块 +const http = require('http') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server (Embedded) starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建HTTP服务器 +const server = http.createServer((req, res) => { + try { + if (req.url === '/' || req.url === '/index.html') { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + // 读取文件内容并发送 + fs.readFile(indexPath, (err, data) => { + if (err) { + console.error('Error reading index.html:', err) + res.writeHead(500) + res.end('Error reading index.html') + return + } + + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(data) + }) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:34515

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } else { + // 处理其他请求 + res.writeHead(404) + res.end('Not found') + } + } catch (error) { + console.error('Error handling request:', error) + res.writeHead(500) + res.end('Server error') + } +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + const port = 34515 + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/public/asr-server/index.html b/public/asr-server/index.html index 00a207f6de..cfa0db82fb 100644 --- a/public/asr-server/index.html +++ b/public/asr-server/index.html @@ -4,7 +4,7 @@ - Browser ASR (External) + Cherry Studio ASR + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:${port}

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } catch (error) { + console.error('Error serving index.html:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +// 创建HTTP服务器 +const server = http.createServer(app) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/public/asr-server/start-server.bat b/public/asr-server/start-server.bat new file mode 100644 index 0000000000..3f9628a75c --- /dev/null +++ b/public/asr-server/start-server.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting ASR Server... +cd /d %~dp0 +node standalone.js +pause diff --git a/resources/asr-server/embedded.js b/resources/asr-server/embedded.js new file mode 100644 index 0000000000..68ae30e239 --- /dev/null +++ b/resources/asr-server/embedded.js @@ -0,0 +1,123 @@ +/** + * 内置的ASR服务器模块 + * 这个文件可以直接在Electron中运行,不需要外部依赖 + */ + +// 使用Electron内置的Node.js模块 +const http = require('http') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server (Embedded) starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建HTTP服务器 +const server = http.createServer((req, res) => { + try { + if (req.url === '/' || req.url === '/index.html') { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + // 读取文件内容并发送 + fs.readFile(indexPath, (err, data) => { + if (err) { + console.error('Error reading index.html:', err) + res.writeHead(500) + res.end('Error reading index.html') + return + } + + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(data) + }) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:34515

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } else { + // 处理其他请求 + res.writeHead(404) + res.end('Not found') + } + } catch (error) { + console.error('Error handling request:', error) + res.writeHead(500) + res.end('Server error') + } +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + const port = 34515 + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/resources/asr-server/index.html b/resources/asr-server/index.html new file mode 100644 index 0000000000..cfa0db82fb --- /dev/null +++ b/resources/asr-server/index.html @@ -0,0 +1,425 @@ + + + + + + + Cherry Studio ASR + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/resources/asr-server/package-lock.json b/resources/asr-server/package-lock.json new file mode 100644 index 0000000000..8d3eb4035d --- /dev/null +++ b/resources/asr-server/package-lock.json @@ -0,0 +1,854 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cherry-asr-server", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/resources/asr-server/package.json b/resources/asr-server/package.json new file mode 100644 index 0000000000..b09528cca8 --- /dev/null +++ b/resources/asr-server/package.json @@ -0,0 +1,10 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "description": "Cherry Studio ASR Server", + "main": "server.js", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } +} \ No newline at end of file diff --git a/resources/asr-server/server.js b/resources/asr-server/server.js new file mode 100644 index 0000000000..ed73ecb022 --- /dev/null +++ b/resources/asr-server/server.js @@ -0,0 +1,269 @@ +// 检查依赖项 +try { + console.log('ASR Server starting...') + console.log('Node.js version:', process.version) + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + console.log('Command line arguments:', process.argv) + + // 检查必要的依赖项 + const checkDependency = (name) => { + try { + require(name) // Removed unused variable 'module' + console.log(`Successfully loaded dependency: ${name}`) + return true + } catch (error) { + console.error(`Failed to load dependency: ${name}`, error.message) + return false + } + } + + // 检查所有必要的依赖项 + const dependencies = ['http', 'ws', 'express', 'path', 'fs'] + const missingDeps = dependencies.filter((dep) => !checkDependency(dep)) + + if (missingDeps.length > 0) { + console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`) + process.exit(1) + } +} catch (error) { + console.error('Error during dependency check:', error) + process.exit(1) +} + +// 加载依赖项 +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module +// const fs = require('fs') // Commented out unused import 'fs' + +const app = express() +const port = 34515 // Define the port + +// 获取index.html文件的路径 +function getIndexHtmlPath() { + const fs = require('fs') + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + + // 尝试多个可能的路径 + const possiblePaths = [ + // 开发环境路径 + path.join(__dirname, 'index.html'), + // 当前目录 + path.join(process.cwd(), 'index.html'), + // 相对于可执行文件的路径 + path.join(path.dirname(process.execPath), 'index.html'), + // 相对于可执行文件的上级目录的路径 + path.join(path.dirname(path.dirname(process.execPath)), 'index.html'), + // 相对于可执行文件的resources目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'index.html'), + // 相对于可执行文件的resources/asr-server目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'), + // 相对于可执行文件的asr-server目录的路径 + path.join(path.dirname(process.execPath), 'asr-server', 'index.html'), + // 如果是pkg打包环境 + process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null + ].filter(Boolean) // 过滤掉null值 + + console.log('Possible index.html paths:', possiblePaths) + + // 检查每个路径,返回第一个存在的文件 + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + console.log(`Found index.html at: ${p}`) + return p + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + // 如果没有找到文件,返回默认路径并记录错误 + console.error('Could not find index.html in any of the expected locations') + return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在 +} + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + const indexPath = getIndexHtmlPath() + console.log(`Serving index.html from: ${indexPath}`) + + // 检查文件是否存在 + const fs = require('fs') + if (!fs.existsSync(indexPath)) { + console.error(`Error: index.html not found at ${indexPath}`) + return res.status(404).send(`Error: index.html not found at ${indexPath}.
Please check the server logs.`) + } + + res.sendFile(indexPath, (err) => { + if (err) { + console.error('Error sending index.html:', err) + res.status(500).send(`Error serving index.html: ${err.message}`) + } + }) + } catch (error) { + console.error('Error in route handler:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = 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') + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = 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') + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } else if (data.type === 'reset' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying RESET command to browser') + browserConnection.send(JSON.stringify({ type: 'reset' })) + } else { + console.log('[Server] Cannot relay RESET: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // Handle server errors + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/resources/asr-server/standalone.js b/resources/asr-server/standalone.js new file mode 100644 index 0000000000..7b826d1f25 --- /dev/null +++ b/resources/asr-server/standalone.js @@ -0,0 +1,114 @@ +/** + * 独立的ASR服务器 + * 这个文件是一个简化版的server.js,用于在打包后的应用中运行 + */ + +// 基本依赖 +const http = require('http') +const express = require('express') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建Express应用 +const app = express() +const port = 34515 + +// 提供静态文件 +app.use(express.static(__dirname)) + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + res.sendFile(indexPath) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.send(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:${port}

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } catch (error) { + console.error('Error serving index.html:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +// 创建HTTP服务器 +const server = http.createServer(app) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/resources/asr-server/start-server.bat b/resources/asr-server/start-server.bat new file mode 100644 index 0000000000..3f9628a75c --- /dev/null +++ b/resources/asr-server/start-server.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting ASR Server... +cd /d %~dp0 +node standalone.js +pause diff --git a/src/main/services/ASRServerService.ts b/src/main/services/ASRServerService.ts index 1aec4c8498..52fa7dd4bd 100644 --- a/src/main/services/ASRServerService.ts +++ b/src/main/services/ASRServerService.ts @@ -37,21 +37,21 @@ class ASRServerService { log.info('App path:', app.getAppPath()) // 在开发环境和生产环境中使用不同的路径 let serverPath = '' - let isExeFile = false + const isPackaged = app.isPackaged - // 首先检查是否有打包后的exe文件 - const exePath = path.join(app.getAppPath(), 'resources', 'cherry-asr-server.exe') - if (fs.existsSync(exePath)) { - serverPath = exePath - isExeFile = true - log.info('检测到打包后的exe文件:', serverPath) - } else if (process.env.NODE_ENV === 'development') { - // 开发环境 - serverPath = path.join(app.getAppPath(), 'src', 'renderer', 'src', 'assets', 'asr-server', 'server.js') + if (isPackaged) { + // 生产环境 (打包后) - 使用 extraResources 复制的路径 + // 注意: 'app' 是 extraResources 配置中 'to' 字段的一部分 + serverPath = path.join(process.resourcesPath, 'app', 'asr-server', 'server.js') + log.info('生产环境,ASR 服务器路径:', serverPath) } else { - // 生产环境 - serverPath = path.join(app.getAppPath(), 'public', 'asr-server', 'server.js') + // 开发环境 - 指向项目根目录的 asr-server + serverPath = path.join(app.getAppPath(), 'asr-server', 'server.js') + log.info('开发环境,ASR 服务器路径:', serverPath) } + + // 注意:删除了 isExeFile 检查逻辑, 假设总是用 node 启动 + // Removed unused variable 'isExeFile' log.info('ASR服务器路径:', serverPath) // 检查文件是否存在 @@ -60,19 +60,12 @@ class ASRServerService { } // 启动服务器进程 - if (isExeFile) { - // 如果是exe文件,直接启动 - this.asrServerProcess = spawn(serverPath, [], { - stdio: 'pipe', - detached: false - }) - } else { - // 如果是js文件,使用node启动 - this.asrServerProcess = spawn('node', [serverPath], { - stdio: 'pipe', - detached: false - }) - } + // 始终使用 node 启动 server.js + log.info(`尝试使用 node 启动: ${serverPath}`) + this.asrServerProcess = spawn('node', [serverPath], { + stdio: 'pipe', // 'pipe' 用于捕获输出, 如果需要调试可以临时改为 'inherit' + detached: false // false 通常足够 + }) // 处理服务器输出 this.asrServerProcess.stdout?.on('data', (data) => { diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts index 4a6fde670d..9e5b7b8c5d 100644 --- a/src/main/utils/index.ts +++ b/src/main/utils/index.ts @@ -4,7 +4,8 @@ import path from 'node:path' import { app } from 'electron' export function getResourcePath() { - return path.join(app.getAppPath(), 'resources') + // 在打包环境中,使用process.resourcesPath,否则使用app.getAppPath()/resources + return app.isPackaged ? process.resourcesPath : path.join(app.getAppPath(), 'resources') } export function getDataPath() { diff --git a/src/renderer/src/assets/asr-server/server.js b/src/renderer/src/assets/asr-server/server.js index 677124ce97..0840ec619d 100644 --- a/src/renderer/src/assets/asr-server/server.js +++ b/src/renderer/src/assets/asr-server/server.js @@ -4,7 +4,7 @@ const express = require('express') const path = require('path') // Need path module const app = express() -const port = 8080 // Define the port +const port = 34515 // Define the port // 获取index.html文件的路径 function getIndexHtmlPath() { diff --git a/src/renderer/src/components/TTSProgressBar.tsx b/src/renderer/src/components/TTSProgressBar.tsx index 0a36e5e068..609cb6ff00 100644 --- a/src/renderer/src/components/TTSProgressBar.tsx +++ b/src/renderer/src/components/TTSProgressBar.tsx @@ -1,4 +1,6 @@ +import { RootState } from '@renderer/store' import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' import styled from 'styled-components' interface TTSProgressBarProps { @@ -13,6 +15,9 @@ interface TTSProgressState { } const TTSProgressBar: React.FC = ({ messageId }) => { + // 获取是否显示TTS进度条的设置 + const showTTSProgressBar = useSelector((state: RootState) => state.settings.showTTSProgressBar) + const [progressState, setProgressState] = useState({ isPlaying: false, progress: 0, @@ -20,22 +25,40 @@ const TTSProgressBar: React.FC = ({ messageId }) => { duration: 0 }) + // 添加拖动状态 + const [isDragging, setIsDragging] = useState(false) + // 监听TTS进度更新事件 useEffect(() => { const handleProgressUpdate = (event: CustomEvent) => { const { messageId: playingMessageId, isPlaying, progress, currentTime, duration } = event.detail - console.log('TTS进度更新事件:', { - playingMessageId, - currentMessageId: messageId, - isPlaying, - progress, - currentTime, - duration - }) + // 不需要每次都输出日志,避免控制台刷屏 + // 只在进度变化较大时输出日志,或者开始/结束时 + // 在拖动进度条时不输出日志 + // 完全关闭进度更新日志输出 + // if (!isDragging && + // playingMessageId === messageId && + // ( + // // 开始或结束播放 + // (isPlaying !== progressState.isPlaying) || + // // 每10%输出一次日志 + // (Math.floor(progress / 10) !== Math.floor(progressState.progress / 10)) + // ) + // ) { + // console.log('TTS进度更新:', { + // messageId: messageId.substring(0, 8), + // isPlaying, + // progress: Math.round(progress), + // currentTime: Math.round(currentTime), + // duration: Math.round(duration) + // }) + // } // 只有当前消息正在播放时才更新进度 - if (playingMessageId === messageId) { + // 增加对playingMessageId的检查,确保它存在且不为空 + // 这样在语音通话模式下的开场白不会显示进度条 + if (playingMessageId && playingMessageId === messageId) { // 如果收到的是重置信号(duration为0),则强制设置为非播放状态 if (duration === 0 && currentTime === 0 && progress === 0) { setProgressState({ @@ -64,7 +87,7 @@ const TTSProgressBar: React.FC = ({ messageId }) => { // 如果停止播放,重置进度条状态 if (!isPlaying && progressState.isPlaying) { - console.log('收到TTS停止播放事件,重置进度条') + // console.log('收到TTS停止播放事件,重置进度条') setProgressState({ isPlaying: false, progress: 0, @@ -78,18 +101,18 @@ const TTSProgressBar: React.FC = ({ messageId }) => { window.addEventListener('tts-progress-update', handleProgressUpdate as EventListener) window.addEventListener('tts-state-change', handleStateChange as EventListener) - console.log('添加TTS进度更新事件监听器,消息ID:', messageId) + // console.log('添加TTS进度更新事件监听器,消息ID:', messageId) // 组件卸载时移除事件监听器 return () => { window.removeEventListener('tts-progress-update', handleProgressUpdate as EventListener) window.removeEventListener('tts-state-change', handleStateChange as EventListener) - console.log('移除TTS进度更新事件监听器,消息ID:', messageId) + // console.log('移除TTS进度更新事件监听器,消息ID:', messageId) } - }, [messageId, progressState.isPlaying]) + }, [messageId, progressState.isPlaying, isDragging]) - // 如果没有播放,不显示进度条 - if (!progressState.isPlaying) { + // 如果没有播放或者设置为不显示进度条,则不显示 + if (!progressState.isPlaying || !showTTSProgressBar) { return null } @@ -106,7 +129,7 @@ const TTSProgressBar: React.FC = ({ messageId }) => { const seekPercentage = (clickPosition / trackWidth) * 100 const seekTime = (seekPercentage / 100) * progressState.duration - console.log(`进度条点击: ${seekPercentage.toFixed(2)}%, 时间: ${seekTime.toFixed(2)}秒`) + // console.log(`进度条点击: ${seekPercentage.toFixed(2)}%, 时间: ${seekTime.toFixed(2)}秒`) // 调用TTS服务的seek方法 import('@renderer/services/TTSService').then(({ default: TTSService }) => { @@ -120,8 +143,8 @@ const TTSProgressBar: React.FC = ({ messageId }) => { e.preventDefault() e.stopPropagation() // 阻止事件冒泡 - // 记录开始拖动状态 - let isDragging = true + // 设置拖动状态为true + setIsDragging(true) const trackRect = e.currentTarget.getBoundingClientRect() const trackWidth = trackRect.width @@ -145,7 +168,8 @@ const TTSProgressBar: React.FC = ({ messageId }) => { const handleMouseUp = (upEvent: MouseEvent) => { if (!isDragging) return - isDragging = false + // 设置拖动状态为false + setIsDragging(false) document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) @@ -153,7 +177,7 @@ const TTSProgressBar: React.FC = ({ messageId }) => { const seekPercentage = (dragPosition / trackWidth) * 100 const seekTime = (seekPercentage / 100) * progressState.duration - console.log(`拖动结束: ${seekPercentage.toFixed(2)}%, 时间: ${seekTime.toFixed(2)}秒`) + // console.log(`拖动结束: ${seekPercentage.toFixed(2)}%, 时间: ${seekTime.toFixed(2)}秒`) // 调用TTS服务的seek方法 import('@renderer/services/TTSService').then(({ default: TTSService }) => { diff --git a/src/renderer/src/config/prompts.ts b/src/renderer/src/config/prompts.ts index fc24387af6..e1ed898d8f 100644 --- a/src/renderer/src/config/prompts.ts +++ b/src/renderer/src/config/prompts.ts @@ -1,5 +1,66 @@ +import i18n from '@renderer/i18n' import dayjs from 'dayjs' +// 语音通话提示词(多语言支持) +export const VOICE_CALL_PROMPTS: Record = { + 'zh-CN': `当前是语音通话模式。请注意: +1. 简洁直接地回答问题,避免冗长的引导和总结。 +2. 避免使用复杂的格式化内容,如表格、代码块、Markdown等。 +3. 使用自然、口语化的表达方式,就像与人对话一样。 +4. 如果需要列出要点,使用简单的数字或文字标记,而不是复杂的格式。 +5. 回答应该简短有力,便于用户通过语音理解。 +6. 避免使用特殊符号、表情符号、标点符号等,因为这些在语音播放时会影响理解。 +7. 使用完整的句子而非简单的关键词列表。 +8. 尽量使用常见词汇,避免生僻或专业术语,除非用户特别询问。`, + 'en-US': `This is voice call mode. Please note: +1. Answer questions concisely and directly, avoiding lengthy introductions and summaries. +2. Avoid complex formatted content such as tables, code blocks, Markdown, etc. +3. Use natural, conversational language as if speaking to a person. +4. If you need to list points, use simple numbers or text markers rather than complex formats. +5. Responses should be brief and powerful, easy for users to understand through voice. +6. Avoid special symbols, emojis, punctuation marks, etc., as these can affect comprehension during voice playback. +7. Use complete sentences rather than simple keyword lists. +8. Try to use common vocabulary, avoiding obscure or technical terms unless specifically asked by the user.`, + 'zh-TW': `當前是語音通話模式。請注意: +1. 簡潔直接地回答問題,避免冗長的引導和總結。 +2. 避免使用複雜的格式化內容,如表格、代碼塊、Markdown等。 +3. 使用自然、口語化的表達方式,就像與人對話一樣。 +4. 如果需要列出要點,使用簡單的數字或文字標記,而不是複雜的格式。 +5. 回答應該簡短有力,便於用戶通過語音理解。 +6. 避免使用特殊符號、表情符號、標點符號等,因為這些在語音播放時會影響理解。 +7. 使用完整的句子而非簡單的關鍵詞列表。 +8. 盡量使用常見詞彙,避免生僻或專業術語,除非用戶特別詢問。`, + 'ja-JP': `これは音声通話モードです。ご注意ください: +1. 質問に簡潔かつ直接的に答え、長い導入や要約を避けてください。 +2. 表、コードブロック、Markdownなどの複雑な書式付きコンテンツを避けてください。 +3. 人と話すように、自然で会話的な言葉を使ってください。 +4. ポイントをリストアップする必要がある場合は、複雑な形式ではなく、単純な数字やテキストマーカーを使用してください。 +5. 応答は簡潔で力強く、ユーザーが音声で理解しやすいものにしてください。 +6. 特殊記号、絵文字、句読点などは、音声再生中に理解に影響を与える可能性があるため、避けてください。 +7. 単純なキーワードリストではなく、完全な文を使用してください。 +8. ユーザーから特に質問されない限り、わかりにくい専門用語を避け、一般的な語彙を使用するようにしてください。`, + 'ru-RU': `Это режим голосового вызова. Обратите внимание: +1. Отвечайте на вопросы кратко и прямо, избегая длинных введений и резюме. +2. Избегайте сложного форматированного содержания, такого как таблицы, блоки кода, Markdown и т.д. +3. Используйте естественный, разговорный язык, как при разговоре с человеком. +4. Если вам нужно перечислить пункты, используйте простые цифры или текстовые маркеры, а не сложные форматы. +5. Ответы должны быть краткими и содержательными, легкими для понимания пользователем через голос. +6. Избегайте специальных символов, эмодзи, знаков препинания и т.д., так как они могут затруднить понимание при воспроизведении голосом. +7. Используйте полные предложения, а не простые списки ключевых слов. +8. Старайтесь использовать общеупотребительную лексику, избегая малоизвестных или технических терминов, если пользователь специально не спрашивает о них.` + // 可以添加更多语言... +} + +// 获取当前语言的默认语音通话提示词 +export function getDefaultVoiceCallPrompt(): string { + const language = i18n.language || 'en-US' + // 如果没有对应语言的提示词,使用英文提示词作为后备 + return VOICE_CALL_PROMPTS[language] || VOICE_CALL_PROMPTS['en-US'] +} + +// 为了向后兼容,保留原来的常量 +export const DEFAULT_VOICE_CALL_PROMPT = getDefaultVoiceCallPrompt() + export const AGENT_PROMPT = ` You are a Prompt Generator. You will integrate user input information into a structured Prompt using Markdown syntax. Please do not use code blocks for output, display directly! diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8845d98e74..1a3d07362a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1418,7 +1418,9 @@ "filter.markdown": "Filter Markdown", "filter.code_blocks": "Filter code blocks", "filter.html_tags": "Filter HTML tags", + "filter.emojis": "Filter emojis", "max_text_length": "Maximum text length", + "show_progress_bar": "Show TTS progress bar", "test": "Test Speech", "help": "Text-to-speech functionality supports converting text to natural-sounding speech.", "learn_more": "Learn more", @@ -1499,7 +1501,9 @@ "transcribe_failed": "Failed to transcribe speech", "no_api_key": "[to be translated]:未设置API密钥", "browser_not_support": "[to be translated]:浏览器不支持语音识别" - } + }, + "auto_start_server": "[to be translated]:启动应用自动开启服务器", + "auto_start_server.help": "[to be translated]:启用后,应用启动时会自动开启语音识别服务器" }, "voice": { "title": "Voice Features", @@ -1514,6 +1518,16 @@ "model.select": "Select Model", "model.current": "Current Model: {{model}}", "model.info": "Select the AI model for voice calls. Different models may provide different voice interaction experiences", + "welcome_message": "Hello, I'm your AI assistant. Please press and hold the talk button to start a conversation.", + "prompt": { + "label": "Voice Call Prompt", + "placeholder": "Enter voice call prompt", + "save": "Save", + "reset": "Reset", + "saved": "Prompt saved", + "reset_done": "Prompt reset", + "info": "This prompt will guide the AI's responses in voice call mode" + }, "asr_tts_info": "Voice call uses the Speech Recognition (ASR) and Text-to-Speech (TTS) settings above", "test": "Test Voice Call", "test_info": "Please use the voice call button on the right side of the input box to test" diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index d5bcec3ee6..d607690b20 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1362,66 +1362,74 @@ "error": { "not_enabled": "音声合成が有効になっていません", "no_edge_voice": "ブラウザ TTSの音声が選択されていません", - "no_api_key": "[to be translated]:未设置API密钥", - "browser_not_support": "[to be translated]:浏览器不支持语音合成", - "no_voice": "[to be translated]:未选择音色", - "no_model": "[to be translated]:未选择模型", - "synthesis_failed": "[to be translated]:语音合成失败", - "play_failed": "[to be translated]:语音播放失败", - "empty_text": "[to be translated]:文本为空", - "general": "[to be translated]:语音合成出现错误", - "unsupported_service_type": "[to be translated]:不支持的服务类型: {{serviceType}}" + "no_api_key": "APIキーが設定されていません", + "browser_not_support": "ブラウザが音声合成をサポートしていません", + "no_voice": "音声が選択されていません", + "no_model": "モデルが選択されていません", + "synthesis_failed": "音声合成に失敗しました", + "play_failed": "音声再生に失敗しました", + "empty_text": "テキストが空です", + "general": "音声合成エラーが発生しました", + "unsupported_service_type": "サポートされていないサービスタイプ: {{serviceType}}" }, "help": "OpenAIのTTS APIを使用するには、APIキーが必要です。ブラウザ TTSはブラウザの機能を使用するため、APIキーは不要です。", "learn_more": "詳細はこちら", - "tab_title": "[to be translated]:语音合成", - "service_type.refresh": "[to be translated]:刷新TTS服务类型设置", - "service_type.refreshed": "[to be translated]:已刷新TTS服务类型设置", - "api_key": "[to be translated]:API密钥", - "api_key.placeholder": "[to be translated]:请输入OpenAI API密钥", - "api_url": "[to be translated]:API地址", - "api_url.placeholder": "[to be translated]:例如:https://api.openai.com/v1/audio/speech", - "edge_voice": "[to be translated]:浏览器 TTS音色", - "edge_voice.loading": "[to be translated]:加载中...", - "edge_voice.refresh": "[to be translated]:刷新可用音色列表", - "edge_voice.not_found": "[to be translated]:未找到匹配的音色", - "voice": "[to be translated]:音色", - "voice.placeholder": "[to be translated]:请选择音色", - "voice_input_placeholder": "[to be translated]:输入音色", - "voice_add": "[to be translated]:添加", - "voice_empty": "[to be translated]:暂无自定义音色,请在下方添加", - "model": "[to be translated]:模型", - "model.placeholder": "[to be translated]:请选择模型", - "model_input_placeholder": "[to be translated]:输入模型", - "model_add": "[to be translated]:添加", - "model_empty": "[to be translated]:暂无自定义模型,请在下方添加", - "filter_options": "[to be translated]:过滤选项", - "filter.thinking_process": "[to be translated]:过滤思考过程", - "filter.markdown": "[to be translated]:过滤Markdown标记", - "filter.code_blocks": "[to be translated]:过滤代码块", - "filter.html_tags": "[to be translated]:过滤HTML标签", - "max_text_length": "[to be translated]:最大文本长度", - "service_type.siliconflow": "[to be translated]:硅基流动", - "service_type.mstts": "[to be translated]:免费在线 TTS", - "siliconflow_api_key": "[to be translated]:硅基流动API密钥", - "siliconflow_api_key.placeholder": "[to be translated]:请输入硅基流动API密钥", - "siliconflow_api_url": "[to be translated]:硅基流动API地址", - "siliconflow_api_url.placeholder": "[to be translated]:例如:https://api.siliconflow.cn/v1/audio/speech", - "siliconflow_voice": "[to be translated]:硅基流动音色", - "siliconflow_voice.placeholder": "[to be translated]:请选择音色", - "siliconflow_model": "[to be translated]:硅基流动模型", - "siliconflow_model.placeholder": "[to be translated]:请选择模型", - "siliconflow_response_format": "[to be translated]:响应格式", - "siliconflow_response_format.placeholder": "[to be translated]:默认为mp3", - "siliconflow_speed": "[to be translated]:语速", - "siliconflow_speed.placeholder": "[to be translated]:默认为1.0", - "edge_voice.available_count": "[to be translated]:可用语音: {{count}}个", - "edge_voice.refreshing": "[to be translated]:正在刷新语音列表...", - "edge_voice.refreshed": "[to be translated]:语音列表已刷新", - "mstts.voice": "[to be translated]:免费在线 TTS音色", - "mstts.output_format": "[to be translated]:输出格式", - "mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。", - "error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色" + "tab_title": "音声合成", + "service_type.refresh": "TTS サービスタイプ設定を更新", + "service_type.refreshed": "TTS サービスタイプ設定が更新されました", + "api_key": "API キー", + "api_key.placeholder": "OpenAI API キーを入力してください", + "api_url": "API アドレス", + "api_url.placeholder": "例:https://api.openai.com/v1/audio/speech", + "edge_voice": "ブラウザ TTS 音声", + "edge_voice.loading": "読み込み中...", + "edge_voice.refresh": "利用可能な音声リストを更新", + "edge_voice.not_found": "一致する音声が見つかりません", + "voice": "音声", + "voice.placeholder": "音声を選択してください", + "voice_input_placeholder": "音声を入力", + "voice_add": "追加", + "voice_empty": "カスタム音声がありません。下に追加してください", + "model": "モデル", + "model.placeholder": "モデルを選択してください", + "model_input_placeholder": "モデルを入力", + "model_add": "追加", + "model_empty": "カスタムモデルがありません。下に追加してください", + "filter_options": "フィルターオプション", + "filter.thinking_process": "思考プロセスをフィルター", + "filter.markdown": "Markdownタグをフィルター", + "filter.code_blocks": "コードブロックをフィルター", + "filter.html_tags": "HTMLタグをフィルター", + "max_text_length": "最大テキスト長", + "service_type.siliconflow": "シリコンフロー", + "service_type.mstts": "無料オンライン TTS", + "siliconflow_api_key": "シリコンフロー API キー", + "siliconflow_api_key.placeholder": "シリコンフロー API キーを入力してください", + "siliconflow_api_url": "シリコンフロー API アドレス", + "siliconflow_api_url.placeholder": "例:https://api.siliconflow.cn/v1/audio/speech", + "siliconflow_voice": "シリコンフロー音声", + "siliconflow_voice.placeholder": "音声を選択してください", + "siliconflow_model": "シリコンフローモデル", + "siliconflow_model.placeholder": "モデルを選択してください", + "siliconflow_response_format": "レスポンス形式", + "siliconflow_response_format.placeholder": "デフォルトはmp3", + "siliconflow_speed": "話す速度", + "siliconflow_speed.placeholder": "デフォルトは1.0", + "edge_voice.available_count": "利用可能な音声: {{count}}個", + "edge_voice.refreshing": "音声リストを更新中...", + "edge_voice.refreshed": "音声リストが更新されました", + "mstts.voice": "無料オンライン TTS 音声", + "mstts.output_format": "出力形式", + "mstts.info": "無料オンラインTTSサービスはAPIキーが不要で、完全に無料で使用できます。", + "error.no_mstts_voice": "無料オンライン TTS 音声が設定されていません", + "play": "音声を再生", + "stop": "再生を停止", + "speak": "音声を再生", + "stop_global": "すべての音声再生を停止", + "stopped": "音声再生を停止しました", + "segmented": "分割", + "segmented_play": "分割再生", + "segmented_playback": "分割再生" }, "asr": { "title": "音声認識", @@ -1467,26 +1475,40 @@ "not_enabled": "音声認識が有効になっていません", "start_failed": "録音の開始に失敗しました", "transcribe_failed": "音声の文字起こしに失敗しました", - "no_api_key": "[to be translated]:未设置API密钥", - "browser_not_support": "[to be translated]:浏览器不支持语音识别" - } + "no_api_key": "APIキーが設定されていません", + "browser_not_support": "ブラウザが音声認識をサポートしていません" + }, + "auto_start_server": "アプリ起動時にサーバーを自動起動", + "auto_start_server.help": "有効にすると、アプリ起動時に音声認識サーバーが自動的に起動します", + "language": "認識言語" }, "voice": { - "title": "[to be translated]:语音功能", - "help": "[to be translated]:语音功能包括文本转语音(TTS)和语音识别(ASR)。", - "learn_more": "[to be translated]:了解更多" + "title": "音声機能", + "help": "音声機能にはテキスト読み上げ(TTS)と音声認識(ASR)が含まれます。", + "learn_more": "詳細を見る" }, "voice_call": { - "tab_title": "[to be translated]:通话功能", - "enable": "[to be translated]:启用语音通话", - "enable.help": "[to be translated]:启用后可以使用语音通话功能与AI进行对话", - "model": "[to be translated]:通话模型", - "model.select": "[to be translated]:选择模型", - "model.current": "[to be translated]:当前模型: {{model}}", - "model.info": "[to be translated]:选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", - "asr_tts_info": "[to be translated]:语音通话使用上面的语音识别(ASR)和语音合成(TTS)设置", - "test": "[to be translated]:测试通话", - "test_info": "[to be translated]:请使用输入框右侧的语音通话按钮进行测试" + "tab_title": "通話機能", + "enable": "音声通話を有効にする", + "enable.help": "有効にすると、音声通話機能を使用してAIと対話できます", + "model": "通話モデル", + "model.select": "モデルを選択", + "model.current": "現在のモデル: {{model}}", + "model.info": "音声通話用のAIモデルを選択します。モデルによって音声対話の体験が異なる場合があります", + "welcome_message": "こんにちは、AIアシスタントです。会話を始めるには、ボタンを長押ししてください。", + "prompt": { + "label": "音声通話プロンプト", + "placeholder": "音声通話プロンプトを入力", + "save": "保存", + "reset": "リセット", + "saved": "プロンプトが保存されました", + "reset_done": "プロンプトがリセットされました", + "info": "このプロンプトは音声通話モードでのAIの応答方法を指導します", + "language_info": "リセットボタンをクリックすると、現在の言語のデフォルトプロンプトが取得されます" + }, + "asr_tts_info": "音声通話は上記の音声認識(ASR)と音声合成(TTS)の設定を使用します", + "test": "音声通話テスト", + "test_info": "入力ボックスの右側にある音声通話ボタンを使用してテストしてください" } }, "translate": { @@ -1529,26 +1551,26 @@ "visualization": "可視化" }, "voice_call": { - "title": "[to be translated]:语音通话", - "start": "[to be translated]:开始语音通话", - "end": "[to be translated]:结束通话", - "mute": "[to be translated]:静音", - "unmute": "[to be translated]:取消静音", - "pause": "[to be translated]:暂停", - "resume": "[to be translated]:继续", - "you": "[to be translated]:您", - "ai": "[to be translated]:AI", - "press_to_talk": "[to be translated]:长按说话", - "release_to_send": "[to be translated]:松开发送", - "initialization_failed": "[to be translated]:初始化语音通话失败", - "error": "[to be translated]:语音通话出错", - "initializing": "[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]:按下此快捷键开始录音,松开快捷键结束录音并发送" + "title": "音声通話", + "start": "音声通話を開始", + "end": "通話を終了", + "mute": "ミュート", + "unmute": "ミュート解除", + "pause": "一時停止", + "resume": "再開", + "you": "あなた", + "ai": "AI", + "press_to_talk": "長押しして話す", + "release_to_send": "離すと送信", + "initialization_failed": "音声通話の初期化に失敗しました", + "error": "音声通話エラー", + "initializing": "音声通話を初期化中...", + "ready": "音声通話の準備が完了しました", + "shortcut_key_setting": "音声認識ショートカットキー設定", + "press_any_key": "任意のキーを押してください...", + "save": "保存", + "cancel": "キャンセル", + "shortcut_key_tip": "このショートカットキーを押すと録音が始まり、キーを離すと録音が終了して送信されます" } } } \ No newline at end of file diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 0833eb7f5c..156e46833b 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1421,7 +1421,15 @@ "mstts.voice": "[to be translated]:免费在线 TTS音色", "mstts.output_format": "[to be translated]:输出格式", "mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。", - "error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色" + "error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色", + "play": "[to be translated]:播放语音", + "stop": "[to be translated]:停止播放", + "speak": "[to be translated]:播放语音", + "stop_global": "[to be translated]:停止所有语音播放", + "stopped": "[to be translated]:已停止语音播放", + "segmented": "[to be translated]:分段", + "segmented_play": "[to be translated]:分段播放", + "segmented_playback": "[to be translated]:分段播放" }, "voice": { "title": "[to be translated]:语音功能", @@ -1474,7 +1482,9 @@ "browser_not_support": "[to be translated]:浏览器不支持语音识别", "start_failed": "[to be translated]:开始录音失败", "transcribe_failed": "[to be translated]:语音识别失败" - } + }, + "auto_start_server": "[to be translated]:启动应用自动开启服务器", + "auto_start_server.help": "[to be translated]:启用后,应用启动时会自动开启语音识别服务器" }, "voice_call": { "tab_title": "[to be translated]:通话功能", @@ -1484,9 +1494,19 @@ "model.select": "[to be translated]:选择模型", "model.current": "[to be translated]:当前模型: {{model}}", "model.info": "[to be translated]:选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", - "asr_tts_info": "[to be translated]:语音通话使用上面的语音识别(ASR)和语音合成(TTS)设置", - "test": "[to be translated]:测试通话", - "test_info": "[to be translated]:请使用输入框右侧的语音通话按钮进行测试" + "prompt": { + "label": "Подсказка для голосового вызова", + "placeholder": "Введите подсказку для голосового вызова", + "save": "Сохранить", + "reset": "Сбросить", + "saved": "Подсказка сохранена", + "reset_done": "Подсказка сброшена", + "info": "Эта подсказка будет направлять ответы ИИ в режиме голосового вызова", + "language_info": "Нажмите кнопку сброса, чтобы получить стандартную подсказку для текущего языка" + }, + "asr_tts_info": "Голосовой вызов использует настройки распознавания речи (ASR) и синтеза речи (TTS), указанные выше", + "test": "Тестировать голосовой вызов", + "test_info": "Используйте кнопку голосового вызова справа от поля ввода для тестирования" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0aac3ddd40..396bed9daf 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1432,7 +1432,9 @@ "filter.markdown": "过滤Markdown标记", "filter.code_blocks": "过滤代码块", "filter.html_tags": "过滤HTML标签", + "filter.emojis": "过滤表情符号", "max_text_length": "最大文本长度", + "show_progress_bar": "显示TTS进度条", "test": "测试语音", "help": "语音合成功能支持将文本转换为自然语音。", "learn_more": "了解更多", @@ -1516,6 +1518,16 @@ "model.select": "选择模型", "model.current": "当前模型: {{model}}", "model.info": "选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", + "welcome_message": "您好,我是您的AI助手,请长按说话按钮进行对话。", + "prompt": { + "label": "语音通话提示词", + "placeholder": "请输入语音通话提示词", + "save": "保存", + "reset": "重置", + "saved": "提示词已保存", + "reset_done": "提示词已重置", + "info": "此提示词将指导AI在语音通话模式下的回复方式" + }, "asr_tts_info": "语音通话使用上面的语音识别(ASR)和语音合成(TTS)设置", "test": "测试通话", "test_info": "请使用输入框右侧的语音通话按钮进行测试" diff --git a/src/renderer/src/i18n/locales/zh-cn.json.bak b/src/renderer/src/i18n/locales/zh-cn.json.bak new file mode 100644 index 0000000000..396bed9daf --- /dev/null +++ b/src/renderer/src/i18n/locales/zh-cn.json.bak @@ -0,0 +1,1576 @@ +{ + "translation": { + "voice_call": { + "title": "语音通话", + "start": "开始语音通话", + "end": "结束通话", + "mute": "静音", + "unmute": "取消静音", + "pause": "暂停", + "resume": "继续", + "you": "您", + "ai": "AI", + "press_to_talk": "长按说话", + "release_to_send": "松开发送", + "initialization_failed": "初始化语音通话失败", + "error": "语音通话出错", + "initializing": "正在初始化语音通话...", + "ready": "语音通话已就绪", + "shortcut_key_setting": "语音识别快捷键设置", + "press_any_key": "请按任意键...", + "save": "保存", + "cancel": "取消", + "shortcut_key_tip": "按下此快捷键开始录音,松开快捷键结束录音并发送" + }, + "agents": { + "add.button": "添加到助手", + "add.knowledge_base": "知识库", + "add.knowledge_base.placeholder": "选择知识库", + "add.name": "名称", + "add.name.placeholder": "输入名称", + "add.prompt": "提示词", + "add.prompt.placeholder": "输入提示词", + "add.title": "创建智能体", + "delete.popup.content": "确定要删除此智能体吗?", + "edit.message.add.title": "添加", + "edit.message.assistant.placeholder": "输入助手消息", + "edit.message.assistant.title": "助手", + "edit.message.empty.content": "会话输入内容不能为空", + "edit.message.group.title": "消息组", + "edit.message.title": "预设消息", + "edit.message.user.placeholder": "输入用户消息", + "edit.message.user.title": "用户", + "edit.model.select.title": "选择模型", + "edit.settings.hide_preset_messages": "隐藏预设消息", + "edit.title": "编辑智能体", + "manage.title": "管理智能体", + "my_agents": "我的智能体", + "search.no_results": "没有找到相关智能体", + "sorting.title": "排序", + "tag.agent": "智能体", + "tag.default": "默认", + "tag.new": "新建", + "tag.system": "系统", + "title": "智能体" + }, + "assistants": { + "title": "助手", + "abbr": "助手", + "settings.title": "助手设置", + "clear.content": "清空话题会删除助手下所有话题和文件,确定要继续吗?", + "clear.title": "清空话题", + "copy.title": "复制助手", + "delete.content": "删除助手会删除所有该助手下的话题和文件,确定要继续吗?", + "delete.title": "删除助手", + "edit.title": "编辑助手", + "save.success": "保存成功", + "save.title": "保存到智能体", + "search": "搜索助手", + "settings.mcp": "MCP 服务器", + "settings.mcp.enableFirst": "请先在 MCP 设置中启用此服务器", + "settings.mcp.title": "MCP 设置", + "settings.mcp.noServersAvailable": "无可用 MCP 服务器。请在设置中添加服务器", + "settings.mcp.description": "默认启用的 MCP 服务器", + "settings.default_model": "默认模型", + "settings.knowledge_base": "知识库设置", + "settings.model": "模型设置", + "settings.preset_messages": "预设消息", + "settings.prompt": "提示词设置", + "settings.reasoning_effort": "思维链长度", + "settings.reasoning_effort.high": "长", + "settings.reasoning_effort.low": "短", + "settings.reasoning_effort.medium": "中", + "settings.reasoning_effort.off": "关", + "settings.reasoning_effort.tip": "仅支持 OpenAI o-series、Anthropic、Grok 推理模型", + "settings.more": "助手设置" + }, + "auth": { + "error": "自动获取密钥失败,请手动获取", + "get_key": "获取", + "get_key_success": "自动获取密钥成功", + "login": "登录", + "oauth_button": "使用{{provider}}登录" + }, + "backup": { + "confirm": "确定要备份数据吗?", + "confirm.button": "选择备份位置", + "content": "备份全部数据,包括聊天记录、设置、知识库等所有数据。请注意,备份过程可能需要一些时间,感谢您的耐心等待。", + "progress": { + "completed": "备份完成", + "compressing": "压缩文件...", + "copying_files": "复制文件... {{progress}}%", + "preparing": "准备备份...", + "title": "备份进度", + "writing_data": "写入数据..." + }, + "title": "数据备份" + }, + "button": { + "add": "添加", + "added": "已添加", + "collapse": "收起", + "manage": "管理", + "select_model": "选择模型", + "show.all": "显示全部", + "update_available": "有可用更新" + }, + "chat": { + "add.assistant.title": "添加助手", + "artifacts.button.download": "下载", + "artifacts.button.openExternal": "外部浏览器打开", + "artifacts.button.preview": "预览", + "artifacts.preview.openExternal.error.content": "外部浏览器打开出错", + "assistant.search.placeholder": "搜索", + "deeply_thought": "已深度思考(用时 {{secounds}} 秒)", + "default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。", + "default.name": "默认助手", + "default.topic.name": "默认话题", + "tts": { + "play": "播放语音", + "stop": "停止播放", + "speak": "播放语音", + "stop_global": "停止所有语音播放", + "stopped": "已停止语音播放" + }, + "history": { + "assistant_node": "助手", + "click_to_navigate": "点击跳转到对应消息", + "coming_soon": "聊天工作流图表即将上线", + "no_messages": "没有找到消息", + "start_conversation": "开始对话以查看聊天流程图", + "title": "聊天历史", + "user_node": "用户", + "view_full_content": "查看完整内容" + }, + "input.auto_resize": "自动调整高度", + "input.clear": "清空消息 {{Command}}", + "input.clear.content": "确定要清除当前会话所有消息吗?", + "input.clear.title": "清空消息", + "input.collapse": "收起", + "input.context_count.tip": "上下文数 / 最大上下文数", + "input.estimated_tokens.tip": "预估 token 数", + "input.expand": "展开", + "input.file_not_supported": "模型不支持此文件类型", + "input.generate_image": "生成图片", + "input.generate_image_not_supported": "模型不支持生成图片", + "input.knowledge_base": "知识库", + "input.new.context": "清除上下文 {{Command}}", + "input.new_topic": "新话题 {{Command}}", + "input.pause": "暂停", + "input.placeholder": "在这里输入消息...", + "input.translating": "翻译中...", + "input.send": "发送", + "input.settings": "设置", + "input.topics": " 话题 ", + "input.translate": "翻译成{{target_language}}", + "input.upload": "上传图片或文档", + "input.upload.upload_from_local": "上传本地文件...", + "input.upload.document": "上传文档(模型不支持图片)", + "input.web_search": "开启网络搜索", + "input.web_search.button.ok": "去设置", + "input.web_search.enable": "开启网络搜索", + "input.web_search.enable_content": "需要先在设置中检查网络搜索连通性", + "message.new.branch": "分支", + "message.new.branch.created": "新分支已创建", + "message.new.context": "清除上下文", + "message.quote": "引用", + "message.regenerate.model": "切换模型", + "message.useful": "有用", + "navigation": { + "first": "已经是第一条消息", + "history": "聊天历史", + "last": "已经是最后一条消息", + "next": "下一条消息", + "prev": "上一条消息", + "top": "回到顶部", + "bottom": "回到底部", + "close": "关闭" + }, + "resend": "重新发送", + "save": "保存", + "settings.code_collapsible": "代码块可折叠", + "settings.code_wrappable": "代码块可换行", + "settings.code_cacheable": "代码块缓存", + "settings.code_cacheable.tip": "缓存代码块可以减少长代码块的渲染时间,但会增加内存占用", + "settings.code_cache_max_size": "缓存上限", + "settings.code_cache_max_size.tip": "允许缓存的字符数上限(千字符),按照高亮后的代码计算。高亮后的代码长度相比于纯文本会长很多。", + "settings.code_cache_ttl": "缓存期限", + "settings.code_cache_ttl.tip": "缓存过期时间(分钟)", + "settings.code_cache_threshold": "缓存阈值", + "settings.code_cache_threshold.tip": "允许缓存的最小代码长度(千字符),超过阈值的代码块才会被缓存", + "settings.context_count": "上下文数", + "settings.context_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10", + "settings.max": "不限", + "settings.max_tokens": "开启消息长度限制", + "settings.max_tokens.confirm": "开启消息长度限制", + "settings.max_tokens.confirm_content": "开启消息长度限制后,单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错", + "settings.max_tokens.tip": "单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错", + "settings.reset": "重置", + "settings.set_as_default": "应用到默认助手", + "settings.show_line_numbers": "代码显示行号", + "settings.temperature": "模型温度", + "settings.temperature.tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7", + "settings.thought_auto_collapse": "思考内容自动折叠", + "settings.thought_auto_collapse.tip": "思考结束后思考内容自动折叠", + "settings.top_p": "Top-P", + "settings.top_p.tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化", + "suggestions.title": "建议的问题", + "thinking": "思考中", + "topics.auto_rename": "生成话题名", + "topics.clear.title": "清空消息", + "topics.copy.image": "复制为图片", + "topics.copy.md": "复制为 Markdown", + "topics.copy.title": "复制", + "topics.delete.shortcut": "按住 {{key}} 可直接删除", + "topics.edit.placeholder": "输入新名称", + "topics.edit.title": "编辑话题名", + "topics.export.image": "导出为图片", + "topics.export.joplin": "导出到 Joplin", + "topics.export.md": "导出为 Markdown", + "topics.export.md.reason": "导出为 Markdown (包含思考)", + "topics.export.notion": "导出到 Notion", + "topics.export.obsidian": "导出到 Obsidian", + "topics.export.obsidian_vault": "保管库", + "topics.export.obsidian_vault_placeholder": "请选择保管库名称", + "topics.export.obsidian_path": "路径", + "topics.export.obsidian_path_placeholder": "请选择路径", + "topics.export.obsidian_atributes": "配置笔记属性", + "topics.export.obsidian_btn": "确定", + "topics.export.obsidian_created": "创建时间", + "topics.export.obsidian_created_placeholder": "请选择创建时间", + "topics.export.obsidian_export_failed": "导出到Obsidian失败", + "topics.export.obsidian_export_success": "导出到Obsidian成功", + "topics.export.obsidian_operate": "处理方式", + "topics.export.obsidian_operate_append": "追加", + "topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆盖)", + "topics.export.obsidian_operate_placeholder": "请选择处理方式", + "topics.export.obsidian_operate_prepend": "前置", + "topics.export.obsidian_source": "来源", + "topics.export.obsidian_source_placeholder": "请输入来源", + "topics.export.obsidian_tags": "标签", + "topics.export.obsidian_tags_placeholder": "请输入标签,多个标签用英文逗号分隔", + "topics.export.obsidian_title": "标题", + "topics.export.obsidian_title_placeholder": "请输入标题", + "topics.export.obsidian_title_required": "标题不能为空", + "topics.export.obsidian_no_vaults": "未找到Obsidian保管库", + "topics.export.obsidian_loading": "加载中...", + "topics.export.obsidian_fetch_error": "获取Obsidian保管库失败", + "topics.export.obsidian_fetch_folders_error": "获取文件夹结构失败", + "topics.export.obsidian_no_vault_selected": "请先选择一个保管库", + "topics.export.obsidian_select_vault_first": "请先选择保管库", + "topics.export.obsidian_root_directory": "根目录", + "topics.export.title": "导出", + "topics.export.word": "导出为 Word", + "topics.export.yuque": "导出到语雀", + "topics.list": "话题列表", + "topics.move_to": "移动到", + "topics.new": "开始新对话", + "topics.pinned": "固定话题", + "topics.prompt": "话题提示词", + "topics.prompt.edit.title": "编辑话题提示词", + "topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词", + "topics.title": "话题", + "topics.unpinned": "取消固定", + "translate": "翻译", + "topics.export.siyuan": "导出到思源笔记", + "topics.export.wait_for_title_naming": "正在生成标题...", + "topics.export.title_naming_success": "标题生成成功", + "topics.export.title_naming_failed": "标题生成失败,使用默认标题" + }, + "code_block": { + "collapse": "收起", + "disable_wrap": "取消换行", + "enable_wrap": "换行", + "expand": "展开" + }, + "common": { + "add": "添加", + "advanced_settings": "高级设置", + "and": "和", + "assistant": "智能体", + "avatar": "头像", + "back": "返回", + "cancel": "取消", + "chat": "聊天", + "clear": "清除", + "close": "关闭", + "confirm": "确认", + "copied": "已复制", + "copy": "复制", + "cut": "剪切", + "default": "默认", + "delete": "删除", + "description": "描述", + "docs": "文档", + "download": "下载", + "duplicate": "复制", + "edit": "编辑", + "expand": "展开", + "collapse": "折叠", + "footnote": "引用内容", + "footnotes": "引用内容", + "fullscreen": "已进入全屏模式,按 F11 退出", + "knowledge_base": "知识库", + "language": "语言", + "model": "模型", + "models": "模型", + "more": "更多", + "name": "名称", + "paste": "粘贴", + "prompt": "提示词", + "provider": "提供商", + "regenerate": "重新生成", + "rename": "重命名", + "reset": "重置", + "save": "保存", + "search": "搜索", + "select": "选择", + "topics": "话题", + "warning": "警告", + "you": "用户", + "reasoning_content": "已深度思考", + "sort": { + "pinyin": "按拼音排序", + "pinyin.asc": "按拼音升序", + "pinyin.desc": "按拼音降序" + } + }, + "docs": { + "title": "帮助文档" + }, + "error": { + "backup.file_format": "备份文件格式错误", + "chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥", + "http": { + "400": "请求错误,请检查请求参数是否正确。如果修改了模型设置,请重置到默认设置", + "401": "身份验证失败,请检查 API 密钥是否正确", + "403": "禁止访问,请翻译具体报错信息查看原因,或联系服务商询问被禁止原因", + "404": "模型不存在或者请求路径错误", + "429": "请求速率超过限制,请稍后再试", + "500": "服务器错误,请稍后再试", + "502": "网关错误,请稍后再试", + "503": "服务不可用,请稍后再试", + "504": "网关超时,请稍后再试" + }, + "model.exists": "模型已存在", + "no_api_key": "API 密钥未配置", + "provider_disabled": "模型提供商未启用", + "render": { + "description": "渲染公式失败,请检查公式格式是否正确", + "title": "渲染错误" + }, + "user_message_not_found": "无法找到原始用户消息", + "unknown": "未知错误" + }, + "export": { + "assistant": "助手", + "attached_files": "附件", + "conversation_details": "会话详情", + "conversation_history": "会话历史", + "created": "创建时间", + "last_updated": "最后更新", + "messages": "消息数", + "user": "用户" + }, + "files": { + "actions": "操作", + "all": "所有文件", + "count": "个文件", + "created_at": "创建时间", + "delete": "删除", + "delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?", + "delete.paintings.warning": "绘图中包含该图片,暂时无法删除", + "delete.title": "删除文件", + "document": "文档", + "edit": "编辑", + "file": "文件", + "image": "图片", + "name": "文件名", + "open": "打开", + "size": "大小", + "text": "文本", + "title": "文件", + "type": "类型" + }, + "gpustack": { + "keep_alive_time.description": "模型在内存中保持的时间(默认:5分钟)", + "keep_alive_time.placeholder": "分钟", + "keep_alive_time.title": "保持活跃时间", + "title": "GPUStack" + }, + "history": { + "continue_chat": "继续聊天", + "locate.message": "定位到消息", + "search.messages": "搜索所有消息", + "search.placeholder": "搜索话题或消息...", + "search.topics.empty": "没有找到相关话题, 点击回车键搜索所有消息", + "title": "话题搜索" + }, + "knowledge": { + "add": { + "title": "添加知识库" + }, + "add_directory": "添加目录", + "add_file": "添加文件", + "add_note": "添加笔记", + "add_sitemap": "站点地图", + "add_url": "添加网址", + "cancel_index": "取消索引", + "chunk_overlap": "重叠大小", + "chunk_overlap_placeholder": "默认值(不建议修改)", + "chunk_overlap_tooltip": "相邻文本块之间重复的内容量,确保分段后的文本块之间仍然有上下文联系,提升模型处理长文本的整体效果", + "chunk_size": "分段大小", + "chunk_size_change_warning": "分段大小和重叠大小修改只针对新添加的内容有效", + "chunk_size_placeholder": "默认值(不建议修改)", + "chunk_size_too_large": "分段大小不能超过模型上下文限制({{max_context}})", + "chunk_size_tooltip": "将文档切割分段,每段的大小,不能超过模型上下文限制", + "clear_selection": "清除选择", + "delete": "删除", + "delete_confirm": "确定要删除此知识库吗?", + "directories": "目录", + "directory_placeholder": "请输入目录路径", + "document_count": "请求文档片段数量", + "document_count_default": "默认", + "document_count_help": "请求文档片段数量越多,附带的信息越多,但需要消耗的 Token 也越多", + "drag_file": "拖拽文件到这里", + "edit_remark": "修改备注", + "edit_remark_placeholder": "请输入备注内容", + "empty": "暂无知识库", + "file_hint": "支持 {{file_types}} 格式", + "index_all": "索引全部", + "index_cancelled": "索引已取消", + "index_started": "索引开始", + "invalid_url": "无效的网址", + "model_info": "模型信息", + "no_bases": "暂无知识库", + "no_match": "未匹配到知识库内容", + "no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库", + "not_set": "未设置", + "not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库", + "notes": "笔记", + "notes_placeholder": "输入此知识库的附加信息或上下文...", + "rename": "重命名", + "search": "搜索知识库", + "search_placeholder": "输入查询内容", + "settings": "知识库设置", + "sitemap_placeholder": "请输入站点地图 URL", + "sitemaps": "网站", + "source": "来源", + "status": "状态", + "status_completed": "已完成", + "status_failed": "失败", + "status_new": "已添加", + "status_pending": "等待中", + "status_processing": "处理中", + "threshold": "匹配度阈值", + "threshold_placeholder": "未设置", + "threshold_too_large_or_small": "阈值不能大于1或小于0", + "threshold_tooltip": "用于衡量用户问题与知识库内容之间的相关性(0-1)", + "title": "知识库", + "topN": "返回结果数量", + "topN__too_large_or_small": "返回结果数量不能大于100或小于1", + "topN_placeholder": "未设置", + "topN_tooltip": "返回的匹配结果数量,数值越大,匹配结果越多,但消耗的 Token 也越多", + "url_added": "网址已添加", + "url_placeholder": "请输入网址, 多个网址用回车分隔", + "urls": "网址" + }, + "languages": { + "arabic": "阿拉伯文", + "chinese": "简体中文", + "chinese-traditional": "繁体中文", + "english": "英文", + "french": "法文", + "german": "德文", + "italian": "意大利文", + "japanese": "日文", + "korean": "韩文", + "portuguese": "葡萄牙文", + "russian": "俄文", + "spanish": "西班牙文" + }, + "lmstudio": { + "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)", + "keep_alive_time.placeholder": "分钟", + "keep_alive_time.title": "保持活跃时间", + "title": "LM Studio" + }, + "mermaid": { + "download": { + "png": "下载 PNG", + "svg": "下载 SVG" + }, + "resize": { + "zoom-in": "放大", + "zoom-out": "缩小" + }, + "tabs": { + "preview": "预览", + "source": "源码" + }, + "title": "Mermaid 图表" + }, + "message": { + "api.check.model.title": "请选择要检测的模型", + "api.connection.failed": "连接失败", + "api.connection.success": "连接成功", + "assistant.added.content": "智能体添加成功", + "attachments": { + "pasted_image": "剪切板图片", + "pasted_text": "剪切板文件" + }, + "backup.failed": "备份失败", + "backup.start.success": "开始备份", + "backup.success": "备份成功", + "chat.completion.paused": "会话已停止", + "citations": "引用内容", + "copied": "已复制", + "copy.failed": "复制失败", + "copy.success": "复制成功", + "error.chunk_overlap_too_large": "分段重叠不能大于分段大小", + "error.dimension_too_large": "内容尺寸过大", + "error.enter.api.host": "请输入您的 API 地址", + "error.enter.api.key": "请输入您的 API 密钥", + "error.enter.model": "请选择一个模型", + "error.enter.name": "请输入知识库名称", + "error.get_embedding_dimensions": "获取嵌入维度失败", + "error.invalid.api.host": "无效的 API 地址", + "error.invalid.api.key": "无效的 API 密钥", + "error.invalid.enter.model": "请选择一个模型", + "error.invalid.proxy.url": "无效的代理地址", + "error.invalid.webdav": "无效的 WebDAV 设置", + "error.joplin.export": "导出 Joplin 失败,请保持 Joplin 已运行并检查连接状态或检查配置", + "error.joplin.no_config": "未配置 Joplin 授权令牌 或 URL", + "error.invalid.nutstore": "无效的坚果云设置", + "error.invalid.nutstore_token": "无效的坚果云 Token", + "error.markdown.export.preconf": "导出Markdown文件到预先设定的路径失败", + "error.markdown.export.specified": "导出Markdown文件失败", + "error.notion.export": "导出 Notion 错误,请检查连接状态并对照文档检查配置", + "error.notion.no_api_key": "未配置 Notion API Key 或 Notion Database ID", + "error.yuque.export": "导出语雀错误,请检查连接状态并对照文档检查配置", + "error.yuque.no_config": "未配置语雀 Token 或 知识库 URL", + "group.delete.content": "删除分组消息会删除用户提问和所有助手的回答", + "group.delete.title": "删除分组消息", + "ignore.knowledge.base": "联网模式开启,忽略知识库", + "info.notion.block_reach_limit": "对话过长,正在分页导出到Notion", + "loading.notion.exporting_progress": "正在导出到Notion ({{current}}/{{total}})...", + "loading.notion.preparing": "正在准备导出到Notion...", + "mention.title": "切换模型回答", + "message.code_style": "代码风格", + "message.delete.content": "确定要删除此消息吗?", + "message.delete.title": "删除消息", + "message.multi_model_style": "多模型回答样式", + "message.multi_model_style.fold": "标签模式", + "message.multi_model_style.fold.compress": "切换到紧凑排列", + "message.multi_model_style.fold.expand": "切换到展开排列", + "message.multi_model_style.grid": "卡片布局", + "message.multi_model_style.horizontal": "横向排列", + "message.multi_model_style.vertical": "纵向堆叠", + "message.style": "消息样式", + "message.style.bubble": "气泡", + "message.style.plain": "简洁", + "regenerate.confirm": "重新生成会覆盖当前消息", + "reset.confirm.content": "确定要重置所有数据吗?", + "reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?", + "reset.double.confirm.title": "数据丢失!!!", + "restore.failed": "恢复失败", + "restore.success": "恢复成功", + "save.success.title": "保存成功", + "searching": "正在联网搜索...", + "success.joplin.export": "成功导出到 Joplin", + "success.markdown.export.preconf": "成功导出 Markdown 文件到预先设定的路径", + "success.markdown.export.specified": "成功导出 Markdown 文件", + "success.notion.export": "成功导出到 Notion", + "success.yuque.export": "成功导出到语雀", + "switch.disabled": "请等待当前回复完成后操作", + "tools": { + "completed": "已完成", + "invoking": "调用中" + }, + "topic.added": "话题添加成功", + "upgrade.success.button": "重启", + "upgrade.success.content": "重启用以完成升级", + "upgrade.success.title": "升级成功", + "warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!", + "warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试", + "error.siyuan.export": "导出思源笔记失败,请检查连接状态并对照文档检查配置", + "error.siyuan.no_config": "未配置思源笔记API地址或令牌", + "success.siyuan.export": "导出到思源笔记成功", + "warn.yuque.exporting": "正在导出语雀, 请勿重复请求导出!", + "warn.siyuan.exporting": "正在导出到思源笔记,请勿重复请求导出!" + }, + "minapp": { + "popup": { + "refresh": "刷新", + "close": "关闭小程序", + "minimize": "最小化小程序", + "devtools": "开发者工具", + "openExternal": "在浏览器中打开", + "rightclick_copyurl": "右键复制URL" + }, + "sidebar.add.title": "添加到侧边栏", + "sidebar.remove.title": "从侧边栏移除", + "sidebar.close.title": "关闭", + "sidebar.closeall.title": "全部关闭", + "sidebar.hide.title": "隐藏小程序", + "title": "小程序" + }, + "miniwindow": { + "clipboard": { + "empty": "剪贴板为空" + }, + "feature": { + "chat": "回答此问题", + "explanation": "解释说明", + "summary": "内容总结", + "translate": "文本翻译" + }, + "footer": { + "copy_last_message": "按 C 键复制", + "backspace_clear": "按 Backspace 清空", + "esc": "按 ESC {{action}}", + "esc_back": "返回", + "esc_close": "关闭" + }, + "input": { + "placeholder": { + "empty": "询问 {{model}} 获取帮助...", + "title": "你想对下方文字做什么" + } + }, + "tooltip": { + "pin": "窗口置顶" + } + }, + "models": { + "add_parameter": "添加参数", + "all": "全部", + "custom_parameters": "自定义参数", + "dimensions": "{{dimensions}} 维", + "edit": "编辑模型", + "embedding": "嵌入", + "embedding_model": "嵌入模型", + "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", + "function_calling": "函数调用", + "no_matches": "无可用模型", + "parameter_name": "参数名称", + "parameter_type": { + "boolean": "布尔值", + "json": "JSON", + "number": "数字", + "string": "文本" + }, + "pinned": "已固定", + "rerank_model": "重排模型", + "rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})", + "rerank_model_tooltip": "在设置->模型服务中点击管理按钮添加", + "search": "搜索模型...", + "stream_output": "流式输出", + "type": { + "embedding": "嵌入", + "free": "免费", + "function_calling": "工具", + "reasoning": "推理", + "rerank": "重排", + "select": "选择模型类型", + "text": "文本", + "vision": "视觉", + "websearch": "联网" + } + }, + "navbar": { + "expand": "伸缩对话框", + "hide_sidebar": "隐藏侧边栏", + "show_sidebar": "显示侧边栏" + }, + "ollama": { + "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)", + "keep_alive_time.placeholder": "分钟", + "keep_alive_time.title": "保持活跃时间", + "title": "Ollama" + }, + "paintings": { + "button.delete.image": "删除图片", + "button.delete.image.confirm": "确定要删除此图片吗?", + "button.new.image": "新建图片", + "guidance_scale": "引导比例", + "guidance_scale_tip": "无分类器指导。控制模型在寻找相关图像时对提示词的遵循程度", + "image.size": "图片尺寸", + "inference_steps": "推理步数", + "inference_steps_tip": "要执行的推理步数。步数越多,质量越高但耗时越长", + "negative_prompt": "反向提示词", + "negative_prompt_tip": "描述你不想在图片中出现的内容", + "number_images": "生成数量", + "number_images_tip": "一次生成的图片数量 (1-4)", + "prompt_enhancement": "提示词增强", + "prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本", + "prompt_placeholder": "描述你想创建的图片,例如:一个宁静的湖泊,夕阳西下,远处是群山", + "regenerate.confirm": "这将覆盖已生成的图片,是否继续?", + "seed": "随机种子", + "seed_tip": "相同的种子和提示词可以生成相似的图片", + "title": "图片" + }, + "plantuml": { + "download": { + "failed": "下载失败,请检查网络", + "png": "下载 PNG", + "svg": "下载 SVG" + }, + "tabs": { + "preview": "预览", + "source": "源码" + }, + "title": "PlantUML 图表" + }, + "prompts": { + "explanation": "帮我解释一下这个概念", + "summarize": "帮我总结一下这段话", + "title": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号" + }, + "provider": { + "aihubmix": "AiHubMix", + "alayanew": "Alaya NeW", + "anthropic": "Anthropic", + "azure-openai": "Azure OpenAI", + "baichuan": "百川", + "baidu-cloud": "百度云千帆", + "copilot": "GitHub Copilot", + "dashscope": "阿里云百炼", + "deepseek": "深度求索", + "dmxapi": "DMXAPI", + "doubao": "火山引擎", + "fireworks": "Fireworks", + "gemini": "Gemini", + "gitee-ai": "Gitee AI", + "github": "GitHub Models", + "gpustack": "GPUStack", + "grok": "Grok", + "groq": "Groq", + "hunyuan": "腾讯混元", + "hyperbolic": "Hyperbolic", + "infini": "无问芯穹", + "jina": "Jina", + "lmstudio": "LM Studio", + "minimax": "MiniMax", + "mistral": "Mistral", + "modelscope": "ModelScope 魔搭", + "moonshot": "月之暗面", + "nvidia": "英伟达", + "o3": "O3", + "ocoolai": "ocoolAI", + "ollama": "Ollama", + "openai": "OpenAI", + "openrouter": "OpenRouter", + "perplexity": "Perplexity", + "ppio": "PPIO 派欧云", + "qwenlm": "QwenLM", + "silicon": "硅基流动", + "stepfun": "阶跃星辰", + "tencent-cloud-ti": "腾讯云TI", + "together": "Together", + "xirang": "天翼云息壤", + "yi": "零一万物", + "zhinao": "360智脑", + "zhipu": "智谱AI", + "voyageai": "Voyage AI", + "qiniu": "七牛云" + }, + "restore": { + "confirm": "确定要恢复数据吗?", + "confirm.button": "选择备份文件", + "content": "恢复操作将使用备份数据覆盖当前所有应用数据。请注意,恢复过程可能需要一些时间,感谢您的耐心等待。", + "progress": { + "completed": "恢复完成", + "copying_files": "复制文件... {{progress}}%", + "extracting": "解压备份...", + "preparing": "准备恢复...", + "reading_data": "读取数据...", + "title": "恢复进度" + }, + "title": "数据恢复" + }, + "settings": { + "about": "关于我们", + "about.checkingUpdate": "正在检查更新...", + "about.checkUpdate": "检查更新", + "about.checkUpdate.available": "立即更新", + "about.contact.button": "邮件", + "about.contact.title": "邮件联系", + "about.description": "一款为创造者而生的 AI 助手", + "about.downloading": "正在下载更新...", + "about.feedback.button": "反馈", + "about.feedback.title": "意见反馈", + "about.license.button": "查看", + "about.license.title": "许可证", + "about.releases.button": "查看", + "about.releases.title": "更新日志", + "about.social.title": "社交账号", + "about.title": "关于我们", + "about.updateAvailable": "发现新版本 {{version}}", + "about.updateError": "更新出错", + "about.updateNotAvailable": "你的软件已是最新版本", + "about.website.button": "查看", + "about.website.title": "官方网站", + "advanced.auto_switch_to_topics": "自动切换到话题", + "advanced.title": "高级设置", + "assistant": "默认助手", + "assistant.model_params": "模型参数", + "assistant.show.icon": "显示模型图标", + "assistant.title": "默认助手", + "data": { + "app_data": "应用数据", + "app_knowledge": "知识库文件", + "app_knowledge.button.delete": "删除文件", + "app_knowledge.remove_all": "删除知识库文件", + "app_knowledge.remove_all_confirm": "删除知识库文件可以减少存储空间占用,但不会删除知识库向量化数据,删除之后将无法打开源文件,是否删除?", + "app_knowledge.remove_all_success": "文件删除成功", + "app_logs": "应用日志", + "clear_cache": { + "button": "清除缓存", + "confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?", + "error": "清除缓存失败", + "success": "缓存清除成功", + "title": "清除缓存" + }, + "data.title": "数据目录", + "divider.basic": "基础数据设置", + "divider.cloud_storage": "云备份设置", + "divider.export_settings": "导出设置", + "divider.third_party": "第三方连接", + "hour_interval_one": "{{count}} 小时", + "hour_interval_other": "{{count}} 小时", + "export_menu": { + "title": "导出菜单设置", + "image": "导出为图片", + "markdown": "导出为Markdown", + "markdown_reason": "导出为Markdown(包含思考)", + "notion": "导出到Notion", + "yuque": "导出到语雀", + "obsidian": "导出到Obsidian", + "siyuan": "导出到思源笔记", + "joplin": "导出到Joplin", + "docx": "导出为Word" + }, + "joplin": { + "check": { + "button": "检查", + "empty_token": "请先输入 Joplin 授权令牌", + "empty_url": "请先输入 Joplin 剪裁服务监听 URL", + "fail": "Joplin 连接验证失败", + "success": "Joplin 连接验证成功" + }, + "help": "在 Joplin 选项中,启用网页剪裁服务(无需安装浏览器插件),确认端口号,并复制授权令牌", + "title": "Joplin 配置", + "token": "Joplin 授权令牌", + "token_placeholder": "请输入 Joplin 授权令牌", + "url": "Joplin 剪裁服务监听 URL", + "url_placeholder": "http://127.0.0.1:41184/" + }, + "markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。", + "markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式", + "markdown_export.help": "若填入,则每次导出时将自动保存到该路径;否则,将弹出保存对话框", + "markdown_export.path": "默认导出路径", + "markdown_export.path_placeholder": "导出路径", + "markdown_export.select": "选择", + "markdown_export.title": "Markdown 导出", + "message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题", + "message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式。", + "minute_interval_one": "{{count}} 分钟", + "minute_interval_other": "{{count}} 分钟", + "notion.api_key": "Notion 密钥", + "notion.api_key_placeholder": "请输入Notion 密钥", + "notion.auto_split": "导出对话时自动分页", + "notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion", + "notion.check": { + "button": "检查", + "empty_api_key": "未配置 API key", + "empty_database_id": "未配置 Database ID", + "error": "连接异常,请检查网络及 API key 和 Database ID 是否正确", + "fail": "连接失败,请检查网络及 API key 和 Database ID 是否正确", + "success": "连接成功" + }, + "notion.database_id": "Notion 数据库 ID", + "notion.database_id_placeholder": "请输入Notion 数据库 ID", + "notion.help": "Notion 配置文档", + "notion.page_name_key": "页面标题字段名", + "notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name", + "notion.split_size": "自动分页大小", + "notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90", + "notion.split_size_placeholder": "请输入每页块数限制(默认90)", + "notion.title": "Notion 配置", + "title": "数据设置", + "webdav": { + "autoSync": "自动备份", + "autoSync.off": "关闭", + "backup.button": "备份到 WebDAV", + "backup.modal.filename.placeholder": "请输入备份文件名", + "backup.modal.title": "备份到 WebDAV", + "host": "WebDAV 地址", + "host.placeholder": "http://localhost:8080", + "hour_interval_one": "{{count}} 小时", + "hour_interval_other": "{{count}} 小时", + "lastSync": "上次备份时间", + "minute_interval_one": "{{count}} 分钟", + "minute_interval_other": "{{count}} 分钟", + "noSync": "等待下次备份", + "password": "WebDAV 密码", + "path": "WebDAV 路径", + "path.placeholder": "/backup", + "restore.button": "从 WebDAV 恢复", + "restore.confirm.content": "从 WebDAV 恢复将会覆盖当前数据,是否继续?", + "restore.confirm.title": "确认恢复", + "restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?", + "restore.modal.select.placeholder": "请选择要恢复的备份文件", + "restore.modal.title": "从 WebDAV 恢复", + "restore.title": "从 WebDAV 恢复", + "syncError": "备份错误", + "syncStatus": "备份状态", + "title": "WebDAV", + "user": "WebDAV 用户名" + }, + "yuque": { + "check": { + "button": "检查", + "empty_repo_url": "请先输入知识库URL", + "empty_token": "请先输入语雀Token", + "fail": "语雀连接验证失败", + "success": "语雀连接验证成功" + }, + "help": "获取语雀 Token", + "repo_url": "知识库 URL", + "repo_url_placeholder": "https://www.yuque.com/username/xxx", + "title": "语雀配置", + "token": "语雀 Token", + "token_placeholder": "请输入语雀Token" + }, + "obsidian": { + "title": "Obsidian 配置", + "default_vault": "默认 Obsidian 仓库", + "default_vault_placeholder": "请选择默认 Obsidian 仓库", + "default_vault_loading": "正在获取 Obsidian 仓库...", + "default_vault_no_vaults": "未找到 Obsidian 仓库", + "default_vault_fetch_error": "获取 Obsidian 仓库失败", + "default_vault_export_failed": "导出失败" + }, + "siyuan": { + "title": "思源笔记配置", + "api_url": "API地址", + "api_url_placeholder": "例如:http://127.0.0.1:6806", + "token": "API令牌", + "token.help": "在思源笔记->设置->关于中获取", + "token_placeholder": "请输入思源笔记令牌", + "box_id": "笔记本ID", + "box_id_placeholder": "请输入笔记本ID", + "root_path": "文档根路径", + "root_path_placeholder": "例如:/CherryStudio", + "check": { + "title": "连接检查", + "button": "检查", + "empty_config": "请填写API地址和令牌", + "success": "连接成功", + "fail": "连接失败,请检查API地址和令牌", + "error": "连接异常,请检查网络连接" + } + }, + "nutstore": { + "title": "坚果云配置", + "isLogin": "已登录", + "notLogin": "未登录", + "login.button": "登录", + "logout.button": "退出登录", + "logout.title": "确定要退出坚果云登录?", + "logout.content": "退出后将无法备份至坚果云和从坚果云恢复", + "checkConnection.name": "检查连接", + "checkConnection.success": "已连接坚果云", + "checkConnection.fail": "坚果云连接失败", + "username": "坚果云用户名", + "path": "坚果云存储路径", + "path.placeholder": "请输入坚果云的存储路径", + "backup.button": "备份到坚果云", + "restore.button": "从坚果云恢复", + "pathSelector.title": "坚果云存储路径", + "pathSelector.return": "返回", + "pathSelector.currentPath": "当前路径", + "new_folder.button.confirm": "确定", + "new_folder.button.cancel": "取消", + "new_folder.button": "新建文件夹" + } + }, + "display.assistant.title": "助手设置", + "display.custom.css": "自定义 CSS", + "display.custom.css.cherrycss": "从 cherrycss.com 获取", + "display.custom.css.placeholder": "/* 这里写自定义CSS */", + "display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏", + "display.sidebar.disabled": "隐藏的图标", + "display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里", + "display.sidebar.files.icon": "显示文件图标", + "display.sidebar.knowledge.icon": "显示知识图标", + "display.sidebar.minapp.icon": "显示小程序图标", + "display.sidebar.painting.icon": "显示绘画图标", + "display.sidebar.title": "侧边栏设置", + "display.sidebar.translate.icon": "显示翻译图标", + "display.sidebar.visible": "显示的图标", + "display.title": "显示设置", + "display.topic.title": "话题设置", + "miniapps": { + "title": "小程序设置", + "disabled": "隐藏的小程序", + "empty": "把要隐藏的小程序从左侧拖拽到这里", + "visible": "显示的小程序", + "cache_settings": "缓存设置", + "cache_title": "小程序缓存数量", + "cache_description": "设置同时保持活跃状态的小程序最大数量", + "reset_tooltip": "重置为默认值", + "display_title": "小程序显示设置", + "sidebar_title": "侧边栏活跃小程序显示设置", + "sidebar_description": "设置侧边栏是否显示活跃的小程序", + "cache_change_notice": "更改将在打开的小程序增减至设定值后生效" + }, + "font_size.title": "消息字体大小", + "general": "常规设置", + "general.avatar.reset": "重置头像", + "general.backup.button": "备份", + "general.backup.title": "数据备份与恢复", + "general.display.title": "显示设置", + "general.emoji_picker": "表情选择器", + "general.image_upload": "图片上传", + "general.auto_check_update.title": "自动检测更新", + "general.reset.button": "重置", + "general.reset.title": "重置数据", + "general.restore.button": "恢复", + "general.title": "常规设置", + "general.user_name": "用户名", + "general.user_name.placeholder": "请输入用户名", + "general.view_webdav_settings": "查看 WebDAV 设置", + "input.auto_translate_with_space": "快速敲击3次空格翻译", + "input.target_language": "目标语言", + "input.target_language.chinese": "简体中文", + "input.target_language.chinese-traditional": "繁体中文", + "input.target_language.english": "英文", + "input.target_language.japanese": "日文", + "input.target_language.russian": "俄文", + "launch.onboot": "开机自动启动", + "launch.title": "启动", + "launch.totray": "启动时最小化到托盘", + "mcp": { + "actions": "操作", + "active": "启用", + "addError": "添加服务器失败", + "addServer": "添加服务器", + "addSuccess": "服务器添加成功", + "args": "参数", + "argsTooltip": "每个参数占一行", + "baseUrlTooltip": "远程 URL 地址", + "command": "命令", + "sse": "服务器发送事件 (sse)", + "streamableHttp": "可流式传输的HTTP (streamableHttp)", + "stdio": "标准输入/输出 (stdio)", + "inMemory": "内存", + "config_description": "配置模型上下文协议服务器", + "deleteError": "删除服务器失败", + "deleteSuccess": "服务器删除成功", + "dependenciesInstall": "安装依赖项", + "dependenciesInstalling": "正在安装依赖项...", + "description": "描述", + "duplicateName": "已存在同名服务器", + "editJson": "编辑JSON", + "editServer": "编辑服务器", + "env": "环境变量", + "envTooltip": "格式:KEY=value,每行一个", + "findMore": "更多 MCP", + "searchNpx": "搜索 MCP", + "install": "安装", + "installError": "安装依赖项失败", + "installSuccess": "依赖项安装成功", + "jsonFormatError": "JSON格式化错误", + "jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确。", + "jsonSaveError": "保存JSON配置失败", + "jsonSaveSuccess": "JSON配置已保存", + "missingDependencies": "缺失,请安装它以继续", + "name": "名称", + "noServers": "未配置服务器", + "newServer": "MCP 服务器", + "npx_list": { + "actions": "操作", + "desc": "搜索并添加 npm 包作为 MCP 服务", + "description": "描述", + "no_packages": "未找到包", + "npm": "NPM", + "package_name": "包名称", + "scope_placeholder": "输入 npm 作用域 (例如 @your-org)", + "scope_required": "请输入 npm 作用域", + "search": "搜索", + "search_error": "搜索失败", + "title": "NPX 包列表", + "usage": "用法", + "version": "版本" + }, + "errors": { + "32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整" + }, + "serverPlural": "服务器", + "serverSingular": "服务器", + "title": "MCP 服务器", + "startError": "启动失败", + "type": "类型", + "updateError": "更新服务器失败", + "updateSuccess": "服务器更新成功", + "url": "URL", + "editMcpJson": "编辑 MCP 配置", + "installHelp": "获取安装帮助", + "tools": { + "inputSchema": "输入参数", + "availableTools": "可用工具", + "noToolsAvailable": "没有可用工具" + }, + "deleteServer": "删除服务器", + "deleteServerConfirm": "确定要删除此服务器吗?", + "registry": "包管理源", + "registryTooltip": "选择用于安装包的源,以解决默认源的网络问题。", + "registryDefault": "默认", + "not_support": "模型不支持", + "user": "用户", + "system": "系统" + }, + "messages.divider": "消息分割线", + "messages.grid_columns": "消息网格展示列数", + "messages.grid_popover_trigger": "网格详情触发", + "messages.grid_popover_trigger.click": "点击显示", + "messages.grid_popover_trigger.hover": "悬停显示", + "messages.input.paste_long_text_as_file": "长文本粘贴为文件", + "messages.input.paste_long_text_threshold": "长文本长度", + "messages.input.send_shortcuts": "发送快捷键", + "messages.input.show_estimated_tokens": "显示预估 Token 数", + "messages.input.title": "输入设置", + "messages.input.enable_quick_triggers": "启用 '/' 和 '@' 触发快捷菜单", + "messages.markdown_rendering_input_message": "Markdown 渲染输入消息", + "messages.math_engine": "数学公式引擎", + "messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens", + "messages.model.title": "模型设置", + "messages.navigation": "对话导航按钮", + "messages.navigation.anchor": "对话锚点", + "messages.navigation.buttons": "上下按钮", + "messages.navigation.none": "不显示", + "messages.title": "消息设置", + "messages.use_serif_font": "使用衬线字体", + "model": "默认模型", + "models.add.add_model": "添加模型", + "models.add.group_name": "分组名称", + "models.add.group_name.placeholder": "例如 ChatGPT", + "models.add.group_name.tooltip": "例如 ChatGPT", + "models.add.model_id": "模型 ID", + "models.add.model_id.placeholder": "必填 例如 gpt-3.5-turbo", + "models.add.model_id.tooltip": "例如 gpt-3.5-turbo", + "models.add.model_name": "模型名称", + "models.add.model_name.placeholder": "例如 GPT-3.5", + "models.check.all": "所有", + "models.check.all_models_passed": "所有模型检查通过", + "models.check.button_caption": "健康检查", + "models.check.disabled": "关闭", + "models.check.enable_concurrent": "并发检查", + "models.check.enabled": "开启", + "models.check.failed": "失败", + "models.check.keys_status_count": "通过:{{count_passed}}个密钥,失败:{{count_failed}}个密钥", + "models.check.model_status_summary": "{{provider}}: {{count_passed}} 个模型完成健康检查(其中 {{count_partial}} 个模型用某些密钥无法访问),{{count_failed}} 个模型完全无法访问。", + "models.check.no_api_keys": "未找到API密钥,请先添加API密钥。", + "models.check.passed": "通过", + "models.check.select_api_key": "选择要使用的API密钥:", + "models.check.single": "单个", + "models.check.start": "开始", + "models.check.title": "模型健康检查", + "models.check.use_all_keys": "使用密钥", + "models.default_assistant_model": "默认助手模型", + "models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型", + "models.empty": "没有模型", + "models.enable_topic_naming": "话题自动重命名", + "models.manage.add_whole_group": "添加整个分组", + "models.manage.remove_whole_group": "移除整个分组", + "models.topic_naming_model": "话题命名模型", + "models.topic_naming_model_description": "自动命名新话题时使用的模型", + "models.topic_naming_model_setting_title": "话题命名模型设置", + "models.topic_naming_prompt": "话题命名提示词", + "models.translate_model": "翻译模型", + "models.translate_model_description": "翻译服务使用的模型", + "models.translate_model_prompt_message": "请输入翻译模型提示词", + "models.translate_model_prompt_title": "翻译模型提示词", + "moresetting": "更多设置", + "moresetting.check.confirm": "确认勾选", + "moresetting.check.warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!", + "moresetting.warn": "风险警告", + "provider": { + "add.name": "提供商名称", + "add.name.placeholder": "例如 OpenAI", + "add.title": "添加提供商", + "add.type": "提供商类型", + "api.url.preview": "预览: {{url}}", + "api.url.reset": "重置", + "api.url.tip": "/结尾忽略v1版本,#结尾强制使用输入地址", + "api_host": "API 地址", + "api_key": "API 密钥", + "api_key.tip": "多个密钥使用逗号分隔", + "api_version": "API 版本", + "charge": "充值", + "check": "检查", + "check_all_keys": "检查所有密钥", + "check_multiple_keys": "检查多个 API 密钥", + "copilot": { + "auth_failed": "Github Copilot 认证失败", + "auth_success": "Github Copilot 认证成功", + "auth_success_title": "认证成功", + "code_failed": "获取 Device Code 失败,请重试", + "code_generated_desc": "请将 Device Code 复制到下面的浏览器链接中", + "code_generated_title": "获取 Device Code", + "confirm_login": "过度使用可能会导致您的 Github 账号遭到封号,请谨慎使用!!!!", + "confirm_title": "风险警告", + "connect": "连接 Github", + "custom_headers": "自定义请求头", + "description": "您的 Github 账号需要订阅 Copilot", + "expand": "展开", + "headers_description": "自定义请求头(json格式)", + "invalid_json": "JSON 格式错误", + "login": "登录 Github", + "logout": "退出 Github", + "logout_failed": "退出失败,请重试", + "logout_success": "已成功退出", + "model_setting": "模型设置", + "open_verification_first": "请先点击上方链接访问验证页面", + "rate_limit": "速率限制", + "tooltip": "使用 Github Copilot 需要先登录 Github" + }, + "delete.content": "确定要删除此模型提供商吗?", + "delete.title": "删除提供商", + "docs_check": "查看", + "docs_more_details": "获取更多详情", + "get_api_key": "点击这里获取密钥", + "is_not_support_array_content": "开启兼容模式", + "no_models_for_check": "没有可以被检查的模型(例如对话模型)", + "not_checked": "未检查", + "remove_duplicate_keys": "移除重复密钥", + "remove_invalid_keys": "删除无效密钥", + "search": "搜索模型平台...", + "search_placeholder": "搜索模型 ID 或名称", + "title": "模型服务" + }, + "proxy": { + "mode": { + "custom": "自定义代理", + "none": "不使用代理", + "system": "系统代理", + "title": "代理模式" + }, + "title": "代理设置" + }, + "proxy.title": "代理地址", + "quickAssistant": { + "click_tray_to_show": "点击托盘图标启动", + "enable_quick_assistant": "启用快捷助手", + "read_clipboard_at_startup": "启动时读取剪贴板", + "title": "快捷助手", + "use_shortcut_to_show": "右键点击托盘图标或使用快捷键启动" + }, + "shortcuts": { + "action": "操作", + "clear_shortcut": "清除快捷键", + "clear_topic": "清空消息", + "copy_last_message": "复制上一条消息", + "key": "按键", + "mini_window": "快捷助手", + "new_topic": "新建话题", + "press_shortcut": "按下快捷键", + "reset_defaults": "重置默认快捷键", + "reset_defaults_confirm": "确定要重置所有快捷键吗?", + "reset_to_default": "重置为默认", + "search_message": "搜索消息", + "show_app": "显示/隐藏应用", + "show_settings": "打开设置", + "title": "快捷方式", + "toggle_new_context": "清除上下文", + "toggle_show_assistants": "切换助手显示", + "toggle_show_topics": "切换话题显示", + "zoom_in": "放大界面", + "zoom_out": "缩小界面", + "zoom_reset": "重置缩放" + }, + "theme.auto": "自动", + "theme.dark": "深色", + "theme.light": "浅色", + "theme.title": "主题", + "theme.window.style.opaque": "不透明窗口", + "theme.window.style.title": "窗口样式", + "theme.window.style.transparent": "透明窗口", + "title": "设置", + "topic.position": "话题位置", + "topic.position.left": "左侧", + "topic.position.right": "右侧", + "topic.show.time": "显示话题时间", + "tray.onclose": "关闭时最小化到托盘", + "tray.show": "显示托盘图标", + "tray.title": "托盘", + "websearch": { + "blacklist": "黑名单", + "blacklist_description": "在搜索结果中不会出现以下网站的结果", + "blacklist_tooltip": "请使用以下格式(换行分隔)\n匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/", + "check": "检查", + "check_failed": "验证失败", + "check_success": "验证成功", + "enhance_mode": "搜索增强模式", + "enhance_mode_tooltip": "使用默认模型提取关键词后搜索", + "overwrite": "覆盖服务商搜索", + "overwrite_tooltip": "强制使用搜索服务商而不是大语言模型进行搜索", + "get_api_key": "点击这里获取密钥", + "no_provider_selected": "请选择搜索服务商后再检查", + "search_max_result": "搜索结果个数", + "search_provider": "搜索服务商", + "search_provider_placeholder": "选择一个搜索服务商", + "subscribe": "黑名单订阅", + "subscribe_update": "立即更新", + "subscribe_add": "添加订阅", + "subscribe_url": "订阅源地址", + "subscribe_name": "替代名字", + "subscribe_name.placeholder": "当下载的订阅源没有名称时所使用的替代名称", + "subscribe_add_success": "订阅源添加成功!", + "subscribe_delete": "删除订阅源", + "search_result_default": "默认", + "search_with_time": "搜索包含日期", + "tavily": { + "api_key": "Tavily API 密钥", + "api_key.placeholder": "请输入 Tavily API 密钥", + "description": "Tavily 是一个为 AI 代理量身定制的搜索引擎,提供实时、准确的结果、智能查询建议和深入的研究能力", + "title": "Tavily" + }, + "title": "网络搜索", + "apikey": "API 密钥", + "free": "免费" + }, + "quickPhrase": { + "title": "快捷短语", + "add": "添加短语", + "edit": "编辑短语", + "titleLabel": "标题", + "contentLabel": "内容", + "titlePlaceholder": "请输入短语标题", + "contentPlaceholder": "请输入短语内容,支持使用变量,然后按Tab键可以快速定位到变量进行修改。比如:\n帮我规划从${from}到${to}的路线,然后发送到${email}。", + "delete": "删除短语", + "deleteConfirm": "删除短语后将无法恢复,是否继续?" + }, + "quickPanel": { + "title": "快捷菜单", + "close": "关闭", + "select": "选择", + "page": "翻页", + "confirm": "确认", + "back": "后退", + "forward": "前进", + "multiple": "多选" + }, + "privacy": { + "title": "隐私设置", + "enable_privacy_mode": "匿名发送错误报告和数据统计" + }, + "voice": { + "title": "语音功能", + "help": "语音功能包括文本转语音(TTS)、语音识别(ASR)和语音通话。", + "learn_more": "了解更多" + }, + "tts": { + "title": "语音合成", + "tab_title": "语音合成", + "enable": "启用语音合成", + "enable.help": "启用后可以将文本转换为语音", + "reset": "重置", + "reset_title": "重置自定义音色和模型", + "reset_confirm": "确定要重置所有自定义音色和模型吗?这将删除所有已添加的自定义项。", + "reset_success": "重置成功", + "reset_help": "如果音色或模型显示异常,可以尝试重置所有自定义项", + "api_settings": "API设置", + "service_type": "服务类型", + "service_type.openai": "OpenAI", + "service_type.edge": "浏览器 TTS", + "service_type.siliconflow": "硅基流动", + "service_type.mstts": "免费在线 TTS", + "service_type.refresh": "刷新TTS服务类型设置", + "service_type.refreshed": "已刷新TTS服务类型设置", + "siliconflow_api_key": "硅基流动API密钥", + "siliconflow_api_key.placeholder": "请输入硅基流动API密钥", + "siliconflow_api_url": "硅基流动API地址", + "siliconflow_api_url.placeholder": "例如:https://api.siliconflow.cn/v1/audio/speech", + "siliconflow_voice": "硅基流动音色", + "siliconflow_voice.placeholder": "请选择音色", + "siliconflow_model": "硅基流动模型", + "siliconflow_model.placeholder": "请选择模型", + "siliconflow_response_format": "响应格式", + "siliconflow_response_format.placeholder": "默认为mp3", + "siliconflow_speed": "语速", + "siliconflow_speed.placeholder": "默认为1.0", + "api_key": "API密钥", + "api_key.placeholder": "请输入OpenAI API密钥", + "api_url": "API地址", + "api_url.placeholder": "例如:https://api.openai.com/v1/audio/speech", + "edge_voice": "浏览器 TTS音色", + "edge_voice.loading": "加载中...", + "edge_voice.refresh": "刷新可用音色列表", + "edge_voice.not_found": "未找到匹配的音色", + "edge_voice.available_count": "可用语音: {{count}}个", + "edge_voice.refreshing": "正在刷新语音列表...", + "edge_voice.refreshed": "语音列表已刷新", + "mstts.voice": "免费在线 TTS音色", + "mstts.output_format": "输出格式", + "mstts.info": "免费在线TTS服务不需要API密钥,完全免费使用。", + "error.no_mstts_voice": "未设置免费在线 TTS音色", + "voice": "音色", + "voice.placeholder": "请选择音色", + "voice_input_placeholder": "输入音色", + "voice_add": "添加", + "voice_empty": "暂无自定义音色,请在下方添加", + "model": "模型", + "model.placeholder": "请选择模型", + "model_input_placeholder": "输入模型", + "model_add": "添加", + "model_empty": "暂无自定义模型,请在下方添加", + "filter_options": "过滤选项", + "filter.thinking_process": "过滤思考过程", + "filter.markdown": "过滤Markdown标记", + "filter.code_blocks": "过滤代码块", + "filter.html_tags": "过滤HTML标签", + "filter.emojis": "过滤表情符号", + "max_text_length": "最大文本长度", + "show_progress_bar": "显示TTS进度条", + "test": "测试语音", + "help": "语音合成功能支持将文本转换为自然语音。", + "learn_more": "了解更多", + "play": "播放语音", + "stop": "停止播放", + "speak": "播放语音", + "stop_global": "停止所有语音播放", + "stopped": "已停止语音播放", + "segmented": "分段", + "segmented_play": "分段播放", + "segmented_playback": "分段播放", + "error": { + "not_enabled": "语音合成功能未启用", + "no_api_key": "未设置API密钥", + "no_voice": "未选择音色", + "no_model": "未选择模型", + "no_edge_voice": "未选择浏览器 TTS音色", + "browser_not_support": "浏览器不支持语音合成", + "synthesis_failed": "语音合成失败", + "play_failed": "语音播放失败", + "empty_text": "文本为空", + "general": "语音合成出现错误", + "unsupported_service_type": "不支持的服务类型: {{serviceType}}" + } + }, + "asr": { + "title": "语音识别", + "tab_title": "语音识别", + "enable": "启用语音识别", + "enable.help": "启用后可以将语音转换为文本", + "service_type": "服务类型", + "service_type.browser": "浏览器", + "service_type.local": "本地服务器", + "api_key": "API密钥", + "api_key.placeholder": "请输入OpenAI API密钥", + "api_url": "API地址", + "api_url.placeholder": "例如:https://api.openai.com/v1/audio/transcriptions", + "model": "模型", + "browser.info": "使用浏览器内置的语音识别功能,无需额外设置", + "local.info": "使用本地服务器和浏览器进行语音识别,需要先启动服务器并打开浏览器页面", + "local.browser_tip": "请在浏览器中打开此页面,并保持浏览器窗口打开", + "local.test_connection": "测试连接", + "local.connection_success": "连接成功", + "local.connection_failed": "连接失败,请确保服务器已启动", + "server.start": "启动服务器", + "server.stop": "停止服务器", + "server.starting": "正在启动服务器...", + "server.started": "服务器已启动", + "server.stopping": "正在停止服务器...", + "server.stopped": "服务器已停止", + "server.already_running": "服务器已经在运行中", + "server.not_running": "服务器未运行", + "server.start_failed": "启动服务器失败", + "server.stop_failed": "停止服务器失败", + "open_browser": "打开浏览器页面", + "test": "测试语音识别", + "test_info": "请在输入框中使用语音识别按钮进行测试", + "start": "开始录音", + "stop": "停止录音", + "preparing": "准备中", + "recording": "正在录音...", + "processing": "正在处理语音...", + "success": "语音识别成功", + "completed": "语音识别完成", + "canceled": "已取消录音", + "auto_start_server": "启动应用自动开启服务器", + "auto_start_server.help": "启用后,应用启动时会自动开启语音识别服务器", + "error": { + "not_enabled": "语音识别功能未启用", + "no_api_key": "未设置API密钥", + "browser_not_support": "浏览器不支持语音识别", + "start_failed": "开始录音失败", + "transcribe_failed": "语音识别失败" + } + }, + "voice_call": { + "tab_title": "通话功能", + "enable": "启用语音通话", + "enable.help": "启用后可以使用语音通话功能与AI进行对话", + "model": "通话模型", + "model.select": "选择模型", + "model.current": "当前模型: {{model}}", + "model.info": "选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", + "welcome_message": "您好,我是您的AI助手,请长按说话按钮进行对话。", + "prompt": { + "label": "语音通话提示词", + "placeholder": "请输入语音通话提示词", + "save": "保存", + "reset": "重置", + "saved": "提示词已保存", + "reset_done": "提示词已重置", + "info": "此提示词将指导AI在语音通话模式下的回复方式" + }, + "asr_tts_info": "语音通话使用上面的语音识别(ASR)和语音合成(TTS)设置", + "test": "测试通话", + "test_info": "请使用输入框右侧的语音通话按钮进行测试" + } + }, + "translate": { + "any.language": "任意语言", + "button.translate": "翻译", + "close": "关闭", + "confirm": { + "content": "翻译后将覆盖原文,是否继续?", + "title": "翻译确认" + }, + "error.failed": "翻译失败", + "error.not_configured": "翻译模型未配置", + "history": { + "clear": "清空历史", + "clear_description": "清空历史将删除所有翻译历史记录,是否继续?", + "delete": "删除", + "empty": "暂无翻译历史", + "title": "翻译历史" + }, + "menu": { + "description": "对当前输入框内容进行翻译" + }, + "input.placeholder": "输入文本进行翻译", + "output.placeholder": "翻译", + "processing": "翻译中...", + "scroll_sync.disable": "关闭滚动同步", + "scroll_sync.enable": "开启滚动同步", + "title": "翻译", + "tooltip.newline": "换行" + }, + "tray": { + "quit": "退出", + "show_mini_window": "快捷助手", + "show_window": "显示窗口" + }, + "words": { + "knowledgeGraph": "知识图谱", + "quit": "退出", + "show_window": "显示窗口", + "visualization": "可视化" + } + } +} diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index def1809219..084143c7f8 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1421,7 +1421,15 @@ "mstts.voice": "[to be translated]:免费在线 TTS音色", "mstts.output_format": "[to be translated]:输出格式", "mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。", - "error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色" + "error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色", + "play": "[to be translated]:播放语音", + "stop": "[to be translated]:停止播放", + "speak": "[to be translated]:播放语音", + "stop_global": "[to be translated]:停止所有语音播放", + "stopped": "[to be translated]:已停止语音播放", + "segmented": "[to be translated]:分段", + "segmented_play": "[to be translated]:分段播放", + "segmented_playback": "[to be translated]:分段播放" }, "voice": { "title": "[to be translated]:语音功能", @@ -1474,7 +1482,9 @@ "browser_not_support": "[to be translated]:浏览器不支持语音识别", "start_failed": "[to be translated]:开始录音失败", "transcribe_failed": "[to be translated]:语音识别失败" - } + }, + "auto_start_server": "[to be translated]:启动应用自动开启服务器", + "auto_start_server.help": "[to be translated]:启用后,应用启动时会自动开启语音识别服务器" }, "voice_call": { "tab_title": "[to be translated]:通话功能", @@ -1484,9 +1494,19 @@ "model.select": "[to be translated]:选择模型", "model.current": "[to be translated]:当前模型: {{model}}", "model.info": "[to be translated]:选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", - "asr_tts_info": "[to be translated]:语音通话使用上面的语音识别(ASR)和语音合成(TTS)设置", - "test": "[to be translated]:测试通话", - "test_info": "[to be translated]:请使用输入框右侧的语音通话按钮进行测试" + "prompt": { + "label": "語音通話提示詞", + "placeholder": "請輸入語音通話提示詞", + "save": "保存", + "reset": "重置", + "saved": "提示詞已保存", + "reset_done": "提示詞已重置", + "info": "此提示詞將指導AI在語音通話模式下的回覆方式", + "language_info": "點擊重置按鈕可獲取當前語言的預設提示詞" + }, + "asr_tts_info": "語音通話使用上面的語音識別(ASR)和語音合成(TTS)設置", + "test": "測試通話", + "test_info": "請使用輸入框右側的語音通話按鈕進行測試" } }, "translate": { diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 7897ef2dda..57a7384a23 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -18,6 +18,7 @@ import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/com import TranslateButton from '@renderer/components/TranslateButton' import VoiceCallButton from '@renderer/components/VoiceCallButton' import { isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' +import { getDefaultVoiceCallPrompt } from '@renderer/config/prompts' import db from '@renderer/databases' import { useAssistant } from '@renderer/hooks/useAssistant' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' @@ -804,22 +805,17 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = ) } - // 添加语音通话专属提示词 - const voiceCallPrompt = `当前是语音通话模式。请注意: -1. 简洁直接地回答问题,避免冗长的引导和总结。 -2. 避免使用复杂的格式化内容,如表格、代码块、Markdown等。 -3. 使用自然、口语化的表达方式,就像与人对话一样。 -4. 如果需要列出要点,使用简单的数字或文字标记,而不是复杂的格式。 -5. 回答应该简短有力,便于用户通过语音理解。 -6. 避免使用特殊符号、表情符号、标点符号等,因为这些在语音播放时会影响理解。 -7. 使用完整的句子而非简单的关键词列表。 -8. 尽量使用常见词汇,避免生僻或专业术语,除非用户特别询问。` + // 获取用户自定义提示词 + const { voiceCallPrompt } = store.getState().settings + + // 使用自定义提示词或当前语言的默认提示词 + const promptToUse = voiceCallPrompt || getDefaultVoiceCallPrompt() // 如果助手已经有提示词,则在其后添加语音通话专属提示词 if (assistantToUse.prompt) { - assistantToUse.prompt += '\n\n' + voiceCallPrompt + assistantToUse.prompt += '\n\n' + promptToUse } else { - assistantToUse.prompt = voiceCallPrompt + assistantToUse.prompt = promptToUse } console.log('为语音通话消息添加了专属提示词') diff --git a/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx index ab5e7fa83c..5373bc5045 100644 --- a/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx +++ b/src/renderer/src/pages/settings/TTSSettings/ASRSettings.tsx @@ -7,6 +7,7 @@ import { setAsrApiUrl, setAsrAutoStartServer, setAsrEnabled, + setAsrLanguage, setAsrModel, setAsrServiceType } from '@renderer/store/settings' @@ -30,6 +31,7 @@ const ASRSettings: FC = () => { const asrApiUrl = useSelector((state: any) => state.settings.asrApiUrl) const asrModel = useSelector((state: any) => state.settings.asrModel || 'whisper-1') const asrAutoStartServer = useSelector((state: any) => state.settings.asrAutoStartServer) + const asrLanguage = useSelector((state: any) => state.settings.asrLanguage || 'zh-CN') // 检查服务器状态 useEffect(() => { @@ -48,6 +50,20 @@ const ASRSettings: FC = () => { // 模型选项 const modelOptions = [{ label: 'whisper-1', value: 'whisper-1' }] + // 语言选项 + const languageOptions = [ + { label: '中文 (Chinese)', value: 'zh-CN' }, + { label: 'English', value: 'en-US' }, + { label: '日本語 (Japanese)', value: 'ja-JP' }, + { label: 'Русский (Russian)', value: 'ru-RU' }, + { label: 'Français (French)', value: 'fr-FR' }, + { label: 'Deutsch (German)', value: 'de-DE' }, + { label: 'Español (Spanish)', value: 'es-ES' }, + { label: 'Italiano (Italian)', value: 'it-IT' }, + { label: 'Português (Portuguese)', value: 'pt-PT' }, + { label: '한국어 (Korean)', value: 'ko-KR' } + ] + return (
@@ -154,7 +170,7 @@ const ASRSettings: FC = () => { @@ -187,6 +203,19 @@ const ASRSettings: FC = () => { {t('settings.asr.local.browser_tip')} + {/* 语言选择 */} + + { (ttsServiceType === 'openai' && (!ttsApiKey || !ttsVoice || !ttsModel)) || (ttsServiceType === 'edge' && !ttsEdgeVoice) || (ttsServiceType === 'siliconflow' && - (!ttsSiliconflowApiKey || !ttsSiliconflowVoice || !ttsSiliconflowModel)) + (!ttsSiliconflowApiKey || !ttsSiliconflowVoice || !ttsSiliconflowModel)) || + (ttsServiceType === 'mstts' && !ttsMsVoice) }> {t('settings.tts.test')} diff --git a/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx index e952bb3efe..dd24729eda 100644 --- a/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx +++ b/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx @@ -1,9 +1,10 @@ -import { InfoCircleOutlined, PhoneOutlined } from '@ant-design/icons' +import { InfoCircleOutlined, PhoneOutlined, ReloadOutlined } from '@ant-design/icons' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import { getModelLogo } from '@renderer/config/models' +import { DEFAULT_VOICE_CALL_PROMPT } from '@renderer/config/prompts' import { useAppDispatch } from '@renderer/store' -import { setVoiceCallEnabled, setVoiceCallModel } from '@renderer/store/settings' -import { Button, Form, Space, Switch, Tooltip as AntTooltip } from 'antd' +import { setVoiceCallEnabled, setVoiceCallModel, setVoiceCallPrompt } from '@renderer/store/settings' +import { Button, Form, Input, Space, Switch, Tooltip as AntTooltip } from 'antd' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -16,6 +17,10 @@ const VoiceCallSettings: FC = () => { // 从 Redux 获取通话功能设置 const voiceCallEnabled = useSelector((state: any) => state.settings.voiceCallEnabled ?? true) const voiceCallModel = useSelector((state: any) => state.settings.voiceCallModel) + const voiceCallPrompt = useSelector((state: any) => state.settings.voiceCallPrompt) + + // 提示词编辑状态 + const [promptText, setPromptText] = useState(voiceCallPrompt || DEFAULT_VOICE_CALL_PROMPT) // 模型选择状态 const [, setIsSelectingModel] = useState(false) @@ -35,6 +40,19 @@ const VoiceCallSettings: FC = () => { } } + // 保存提示词 + const handleSavePrompt = () => { + dispatch(setVoiceCallPrompt(promptText)) + window.message.success({ content: t('settings.voice_call.prompt.saved'), key: 'voice-call-prompt' }) + } + + // 重置提示词 + const handleResetPrompt = () => { + setPromptText(DEFAULT_VOICE_CALL_PROMPT) + dispatch(setVoiceCallPrompt(null)) + window.message.success({ content: t('settings.voice_call.prompt.reset_done'), key: 'voice-call-prompt' }) + } + return ( @@ -71,6 +89,26 @@ const VoiceCallSettings: FC = () => { {t('settings.voice_call.model.info')} + {/* 提示词设置 */} + + setPromptText(e.target.value)} + disabled={!voiceCallEnabled} + rows={8} + placeholder={t('settings.voice_call.prompt.placeholder')} + /> + + + + + {t('settings.voice_call.prompt.info')} + + {/* ASR 和 TTS 设置提示 */} {t('settings.voice_call.asr_tts_info')} diff --git a/src/renderer/src/services/ASRServerService.ts b/src/renderer/src/services/ASRServerService.ts index 506715a4e8..e31b89bf32 100644 --- a/src/renderer/src/services/ASRServerService.ts +++ b/src/renderer/src/services/ASRServerService.ts @@ -14,13 +14,19 @@ class ASRServerService { startServer = async (): Promise => { if (this.isServerRunning) { console.log('[ASRServerService] 服务器已经在运行中') - window.message.info({ content: i18n.t('settings.asr.server.already_running'), key: 'asr-server' }) + // 安全地调用window.message + if (window.message) { + window.message.info({ content: i18n.t('settings.asr.server.already_running'), key: 'asr-server' }) + } return true } try { console.log('[ASRServerService] 正在启动ASR服务器...') - window.message.loading({ content: i18n.t('settings.asr.server.starting'), key: 'asr-server' }) + // 安全地调用window.message + if (window.message) { + window.message.loading({ content: i18n.t('settings.asr.server.starting'), key: 'asr-server' }) + } // 使用IPC调用主进程启动服务器 const result = await window.api.asrServer.startServer() @@ -29,22 +35,28 @@ class ASRServerService { 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' }) + if (window.message) { + 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' - }) + if (window.message) { + window.message.error({ + content: i18n.t('settings.asr.server.start_failed') + ': ' + result.error, + key: 'asr-server' + }) + } return false } } 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' - }) + if (window.message) { + window.message.error({ + content: i18n.t('settings.asr.server.start_failed') + ': ' + (error as Error).message, + key: 'asr-server' + }) + } return false } } @@ -56,13 +68,17 @@ class ASRServerService { 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' }) + if (window.message) { + 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' }) + if (window.message) { + window.message.loading({ content: i18n.t('settings.asr.server.stopping'), key: 'asr-server' }) + } // 使用IPC调用主进程停止服务器 const result = await window.api.asrServer.stopServer(this.serverProcess) @@ -71,22 +87,28 @@ class ASRServerService { this.isServerRunning = false this.serverProcess = null console.log('[ASRServerService] ASR服务器已停止') - window.message.success({ content: i18n.t('settings.asr.server.stopped'), key: 'asr-server' }) + if (window.message) { + 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' - }) + if (window.message) { + 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' - }) + if (window.message) { + window.message.error({ + content: i18n.t('settings.asr.server.stop_failed') + ': ' + (error as Error).message, + key: 'asr-server' + }) + } return false } } @@ -104,7 +126,8 @@ class ASRServerService { * @returns string 网页URL */ getServerUrl = (): string => { - return 'http://localhost:8080' + console.log('[ASRServerService] 获取服务器URL: http://localhost:34515') + return 'http://localhost:34515' } /** diff --git a/src/renderer/src/services/ASRService.ts b/src/renderer/src/services/ASRService.ts index 5431040e55..143aa5185a 100644 --- a/src/renderer/src/services/ASRService.ts +++ b/src/renderer/src/services/ASRService.ts @@ -66,7 +66,7 @@ class ASRService { console.log('[ASRService] 正在连接WebSocket服务器...') window.message.loading({ content: '正在连接语音识别服务...', key: 'ws-connect' }) - this.ws = new WebSocket('ws://localhost:8080') + this.ws = new WebSocket('ws://localhost:34515') // 使用正确的端口 34515 this.wsConnected = false this.browserReady = false @@ -253,6 +253,9 @@ class ASRService { throw new Error('无法连接到语音识别服务') } + // 获取语言设置 + const { asrLanguage } = store.getState().settings + // 检查浏览器是否准备好 if (!this.browserReady) { // 尝试等待浏览器准备好 @@ -270,7 +273,7 @@ class ASRService { // 尝试自动打开浏览器页面 try { // 使用ASRServerService获取服务器URL - const serverUrl = 'http://localhost:8080' + const serverUrl = 'http://localhost:34515' // 使用正确的端口 34515 console.log('尝试打开语音识别服务器页面:', serverUrl) window.open(serverUrl, '_blank') } catch (error) { @@ -302,9 +305,15 @@ class ASRService { // 发送开始命令 if (this.ws && this.wsConnected) { - this.ws.send(JSON.stringify({ type: 'start' })) + // 将语言设置传递给服务器 + this.ws.send( + JSON.stringify({ + type: 'start', + language: asrLanguage || 'zh-CN' // 使用设置的语言或默认中文 + }) + ) this.isRecording = true - console.log('开始语音识别') + console.log('开始语音识别,语言:', asrLanguage || 'zh-CN') window.message.info({ content: i18n.t('settings.asr.recording'), key: 'asr-recording' }) } else { throw new Error('WebSocket连接未就绪') @@ -638,7 +647,7 @@ class ASRService { */ openBrowserPage = (): void => { // 使用window.open打开浏览器页面 - window.open('http://localhost:8080', '_blank') + window.open('http://localhost:34515', '_blank') // 使用正确的端口 34515 } } diff --git a/src/renderer/src/services/VoiceCallService.ts b/src/renderer/src/services/VoiceCallService.ts index ecf6836715..5742500e61 100644 --- a/src/renderer/src/services/VoiceCallService.ts +++ b/src/renderer/src/services/VoiceCallService.ts @@ -1,3 +1,4 @@ +import { DEFAULT_VOICE_CALL_PROMPT } from '@renderer/config/prompts' import { fetchChatCompletion } from '@renderer/services/ApiService' import ASRService from '@renderer/services/ASRService' import { getDefaultAssistant } from '@renderer/services/AssistantService' @@ -5,8 +6,10 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { getAssistantMessage, getUserMessage } from '@renderer/services/MessagesService' import TTSService from '@renderer/services/TTSService' import store from '@renderer/store' +import { setSkipNextAutoTTS } from '@renderer/store/settings' // 导入类型 import type { Message } from '@renderer/types' +import i18n from 'i18next' interface VoiceCallCallbacks { onTranscript: (text: string) => void @@ -170,9 +173,13 @@ class VoiceCallServiceClass { } } - // 播放欢迎语音 - const welcomeMessage = '您好,我是您的AI助手,请长按说话按钮进行对话。' - this.callbacks?.onResponse(welcomeMessage) + // 设置skipNextAutoTTS为true,防止自动播放最后一条消息 + store.dispatch(setSkipNextAutoTTS(true)) + + // 播放欢迎语音 - 根据当前语言获取本地化的欢迎消息 + const welcomeMessage = i18n.t('settings.voice_call.welcome_message') + // 不调用onResponse,避免触发两次TTS播放 + // this.callbacks?.onResponse(welcomeMessage) // 监听TTS状态 const ttsStateHandler = (isPlaying: boolean) => { @@ -583,21 +590,16 @@ class VoiceCallServiceClass { } }) - // 修改用户消息,添加语音通话提示 - const voiceCallPrompt = `当前是语音通话模式。请注意: -1. 简洁直接地回答问题,避免冗长的引导和总结。 -2. 避免使用复杂的格式化内容,如表格、代码块、Markdown等。 -3. 使用自然、口语化的表达方式,就像与人对话一样。 -4. 如果需要列出要点,使用简单的数字或文字标记,而不是复杂的格式。 -5. 回答应该简短有力,便于用户通过语音理解。 -6. 避免使用特殊符号、表情符号、标点符号等,因为这些在语音播放时会影响理解。 -7. 使用完整的句子而非简单的关键词列表。 -8. 尽量使用常见词汇,避免生僻或专业术语,除非用户特别询问。` + // 获取用户自定义提示词 + const { voiceCallPrompt } = store.getState().settings + + // 使用自定义提示词或默认提示词 + const promptToUse = voiceCallPrompt || DEFAULT_VOICE_CALL_PROMPT // 创建系统指令消息 const systemMessage = { role: 'system', - content: voiceCallPrompt + content: promptToUse } // 修改用户消息的内容 @@ -646,8 +648,12 @@ class VoiceCallServiceClass { // 添加事件监听器 window.addEventListener('tts-state-change', handleTTSStateChange as EventListener) - // 开始播放 - this.ttsService.speak(fullResponse) + // 更新助手消息的内容 + assistantMessage.content = fullResponse + assistantMessage.status = 'success' + + // 使用speakFromMessage方法播放,会应用TTS过滤选项 + this.ttsService.speakFromMessage(assistantMessage) // 设置超时安全机制,确保事件监听器被移除 setTimeout(() => { @@ -666,7 +672,17 @@ class VoiceCallServiceClass { if (!this.isMuted && this.isCallActive) { // 手动设置语音状态 this.callbacks?.onSpeakingStateChange(true) - this.ttsService.speak(fullResponse) + + // 创建一个简单的助手消息对象 + const errorMessage = { + id: 'error-message', + role: 'assistant', + content: fullResponse, + status: 'success' + } as Message + + // 使用speakFromMessage方法播放,会应用TTS过滤选项 + this.ttsService.speakFromMessage(errorMessage) // 确保语音结束后状态正确 setTimeout(() => { diff --git a/src/renderer/src/services/tts/MsTTSService.ts b/src/renderer/src/services/tts/MsTTSService.ts index 7925ace53d..0551f51573 100644 --- a/src/renderer/src/services/tts/MsTTSService.ts +++ b/src/renderer/src/services/tts/MsTTSService.ts @@ -1,5 +1,3 @@ -import i18n from '@renderer/i18n' - import { TTSServiceInterface } from './TTSServiceInterface' /** @@ -27,7 +25,15 @@ export class MsTTSService implements TTSServiceInterface { */ private validateParams(): void { if (!this.voice) { - throw new Error(i18n.t('settings.tts.error.no_mstts_voice')) + // 如果没有设置音色,使用默认的小晓音色 + console.warn('未设置免费在线TTS音色,使用默认音色 zh-CN-XiaoxiaoNeural') + this.voice = 'zh-CN-XiaoxiaoNeural' + } + + if (!this.outputFormat) { + // 如果没有设置输出格式,使用默认格式 + console.warn('未设置免费在线TTS输出格式,使用默认格式 audio-24khz-48kbitrate-mono-mp3') + this.outputFormat = 'audio-24khz-48kbitrate-mono-mp3' } } diff --git a/src/renderer/src/services/tts/TTSService.ts b/src/renderer/src/services/tts/TTSService.ts index d20c12577f..e982bc1397 100644 --- a/src/renderer/src/services/tts/TTSService.ts +++ b/src/renderer/src/services/tts/TTSService.ts @@ -104,6 +104,7 @@ export class TTSService { filterMarkdown: true, filterCodeBlocks: true, filterHtmlTags: true, + filterEmojis: true, maxTextLength: 4000 } @@ -592,7 +593,7 @@ export class TTSService { // 触发进度更新事件 this.emitProgressUpdateEvent(currentTime, duration, progress) } - }, 100) + }, 250) // 将更新频率从100ms降低到250ms,减少日志输出 } /** @@ -611,6 +612,11 @@ export class TTSService { * @param duration 总时长(秒) * @param progress 进度百分比(0-100) */ + // 记录上次输出日志的进度百分比 - 已禁用日志输出 + // private lastLoggedProgress: number = -1; + // 记录上次日志输出时间,用于节流 - 已禁用日志输出 + // private lastLogTime: number = 0; + private emitProgressUpdateEvent(currentTime: number, duration: number, progress: number): void { // 创建事件数据 const eventData = { @@ -621,12 +627,21 @@ export class TTSService { progress } - console.log('发送TTS进度更新事件:', { - messageId: this.playingMessageId, - progress: Math.round(progress), - currentTime: Math.round(currentTime), - duration: Math.round(duration) - }) + // 完全关闭进度更新日志输出 + // const now = Date.now(); + // const currentProgressTens = Math.floor(progress / 10); + // if ((now - this.lastLogTime >= 500) && // 时间节流 + // (currentProgressTens !== Math.floor(this.lastLoggedProgress / 10) || + // progress === 0 || progress >= 100)) { + // console.log('发送TTS进度更新事件:', { + // messageId: this.playingMessageId ? this.playingMessageId.substring(0, 8) : null, + // progress: Math.round(progress), + // currentTime: Math.round(currentTime), + // duration: Math.round(duration) + // }); + // this.lastLoggedProgress = progress; + // this.lastLogTime = now; + // } // 触发事件 window.dispatchEvent(new CustomEvent('tts-progress-update', { detail: eventData })) diff --git a/src/renderer/src/services/tts/TTSServiceFactory.ts b/src/renderer/src/services/tts/TTSServiceFactory.ts index 9d8fb0c4c7..215e5d40f1 100644 --- a/src/renderer/src/services/tts/TTSServiceFactory.ts +++ b/src/renderer/src/services/tts/TTSServiceFactory.ts @@ -48,14 +48,17 @@ export class TTSServiceFactory { settings.ttsSiliconflowSpeed ) - case 'mstts': + case 'mstts': { console.log('创建免费在线TTS服务实例') + // 确保音色有默认值 + const msVoice = settings.ttsMsVoice || 'zh-CN-XiaoxiaoNeural' + const msOutputFormat = settings.ttsMsOutputFormat || 'audio-24khz-48kbitrate-mono-mp3' console.log('免费在线TTS设置:', { - voice: settings.ttsMsVoice, - outputFormat: settings.ttsMsOutputFormat + voice: msVoice, + outputFormat: msOutputFormat }) - return new MsTTSService(settings.ttsMsVoice, settings.ttsMsOutputFormat) - + return new MsTTSService(msVoice, msOutputFormat) + } // Close block scope default: throw new Error(i18n.t('settings.tts.error.unsupported_service_type', { serviceType })) } diff --git a/src/renderer/src/services/tts/TTSTextFilter.ts b/src/renderer/src/services/tts/TTSTextFilter.ts index 023d3b93a1..72962e2951 100644 --- a/src/renderer/src/services/tts/TTSTextFilter.ts +++ b/src/renderer/src/services/tts/TTSTextFilter.ts @@ -16,6 +16,7 @@ export class TTSTextFilter { filterMarkdown: boolean filterCodeBlocks: boolean filterHtmlTags: boolean + filterEmojis: boolean maxTextLength: number } ): string { @@ -43,6 +44,11 @@ export class TTSTextFilter { filteredText = this.filterHtmlTags(filteredText) } + // 过滤表情符号 + if (options.filterEmojis) { + filteredText = this.filterEmojis(filteredText) + } + // 限制文本长度 if (options.maxTextLength > 0 && filteredText.length > options.maxTextLength) { filteredText = filteredText.substring(0, options.maxTextLength) @@ -145,4 +151,18 @@ export class TTSTextFilter { return text } + + /** + * 过滤表情符号 + * @param text 原始文本 + * @returns 过滤后的文本 + */ + private static filterEmojis(text: string): string { + // 过滤Unicode表情符号 + // 这个正则表达式匹配大多数常见的表情符号 + return text.replace( + /[\u{1F300}-\u{1F5FF}\u{1F600}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, + '' + ) + } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 6377049dd3..f8942fae36 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -119,6 +119,7 @@ export interface SettingsState { ttsModel: string ttsCustomVoices: string[] ttsCustomModels: string[] + showTTSProgressBar: boolean // 是否显示TTS进度条 // 浏览器 TTS配置 ttsEdgeVoice: string // 硅基流动 TTS配置 @@ -137,6 +138,7 @@ export interface SettingsState { filterMarkdown: boolean // 过滤Markdown标记 filterCodeBlocks: boolean // 过滤代码块 filterHtmlTags: boolean // 过滤HTML标签 + filterEmojis: boolean // 过滤表情符号 maxTextLength: number // 最大文本长度 } // ASR配置(语音识别) @@ -146,9 +148,11 @@ export interface SettingsState { asrApiUrl: string asrModel: string asrAutoStartServer: boolean // 启动应用时自动启动ASR服务器 + asrLanguage: string // 语音识别语言 // 语音通话配置 voiceCallEnabled: boolean voiceCallModel: Model | null + voiceCallPrompt: string | null // 语音通话自定义提示词 isVoiceCallActive: boolean // 语音通话窗口是否激活 lastPlayedMessageId: string | null // 最后一次播放的消息ID skipNextAutoTTS: boolean // 是否跳过下一次自动TTS @@ -262,6 +266,7 @@ export const initialState: SettingsState = { ttsModel: '', ttsCustomVoices: [], ttsCustomModels: [], + showTTSProgressBar: true, // 默认显示TTS进度条 // Edge TTS配置 ttsEdgeVoice: 'zh-CN-XiaoxiaoNeural', // 默认使用小小的声音 // 硅基流动 TTS配置 @@ -279,6 +284,7 @@ export const initialState: SettingsState = { filterMarkdown: true, // 默认过滤Markdown标记 filterCodeBlocks: true, // 默认过滤代码块 filterHtmlTags: true, // 默认过滤HTML标签 + filterEmojis: true, // 默认过滤表情符号 maxTextLength: 4000 // 默认最大文本长度 }, // ASR配置(语音识别) @@ -288,9 +294,11 @@ export const initialState: SettingsState = { asrApiUrl: 'https://api.openai.com/v1/audio/transcriptions', asrModel: 'whisper-1', asrAutoStartServer: false, // 默认不自动启动ASR服务器 + asrLanguage: 'zh-CN', // 默认使用中文 // 语音通话配置 voiceCallEnabled: true, voiceCallModel: null, + voiceCallPrompt: null, // 默认为null,表示使用默认提示词 isVoiceCallActive: false, // 语音通话窗口是否激活 lastPlayedMessageId: null, // 最后一次播放的消息ID skipNextAutoTTS: false, // 是否跳过下一次自动TTS @@ -692,6 +700,7 @@ const settingsSlice = createSlice({ filterMarkdown?: boolean filterCodeBlocks?: boolean filterHtmlTags?: boolean + filterEmojis?: boolean maxTextLength?: number }> ) => { @@ -700,6 +709,10 @@ const settingsSlice = createSlice({ ...action.payload } }, + // 设置是否显示TTS进度条 + setShowTTSProgressBar: (state, action: PayloadAction) => { + state.showTTSProgressBar = action.payload + }, // ASR相关的action setAsrEnabled: (state, action: PayloadAction) => { state.asrEnabled = action.payload @@ -719,12 +732,18 @@ const settingsSlice = createSlice({ setAsrAutoStartServer: (state, action: PayloadAction) => { state.asrAutoStartServer = action.payload }, + setAsrLanguage: (state, action: PayloadAction) => { + state.asrLanguage = action.payload + }, setVoiceCallEnabled: (state, action: PayloadAction) => { state.voiceCallEnabled = action.payload }, setVoiceCallModel: (state, action: PayloadAction) => { state.voiceCallModel = action.payload }, + setVoiceCallPrompt: (state, action: PayloadAction) => { + state.voiceCallPrompt = action.payload + }, setIsVoiceCallActive: (state, action: PayloadAction) => { state.isVoiceCallActive = action.payload }, @@ -851,14 +870,17 @@ export const { removeTtsCustomVoice, removeTtsCustomModel, setTtsFilterOptions, + setShowTTSProgressBar, setAsrEnabled, setAsrServiceType, setAsrApiKey, setAsrApiUrl, setAsrModel, setAsrAutoStartServer, + setAsrLanguage, setVoiceCallEnabled, setVoiceCallModel, + setVoiceCallPrompt, setIsVoiceCallActive, setLastPlayedMessageId, setSkipNextAutoTTS diff --git a/yarn.lock b/yarn.lock index 19315811d5..bf0b27494c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3945,6 +3945,7 @@ __metadata: axios: "npm:^1.7.3" babel-plugin-styled-components: "npm:^2.1.4" browser-image-compression: "npm:^2.0.2" + bufferutil: "npm:^4.0.9" color: "npm:^5.0.0" dayjs: "npm:^1.11.11" dexie: "npm:^4.0.8" @@ -4021,9 +4022,11 @@ __metadata: turndown-plugin-gfm: "npm:^1.0.2" typescript: "npm:^5.6.2" undici: "npm:^7.4.0" + utf-8-validate: "npm:^6.0.5" uuid: "npm:^10.0.0" vite: "npm:^5.0.12" webdav: "npm:^5.8.0" + ws: "npm:^8.18.1" zipread: "npm:^1.3.3" languageName: unknown linkType: soft @@ -4938,6 +4941,16 @@ __metadata: languageName: node linkType: hard +"bufferutil@npm:^4.0.9": + version: 4.0.9 + resolution: "bufferutil@npm:4.0.9" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10c0/f8a93279fc9bdcf32b42eba97edc672b39ca0fe5c55a8596099886cffc76ea9dd78e0f6f51ecee3b5ee06d2d564aa587036b5d4ea39b8b5ac797262a363cdf7d + languageName: node + linkType: hard + "builder-util-runtime@npm:9.2.4": version: 9.2.4 resolution: "builder-util-runtime@npm:9.2.4" @@ -12107,6 +12120,17 @@ __metadata: languageName: node linkType: hard +"node-gyp-build@npm:^4.3.0": + version: 4.8.4 + resolution: "node-gyp-build@npm:4.8.4" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10c0/444e189907ece2081fe60e75368784f7782cfddb554b60123743dfb89509df89f1f29c03bbfa16b3a3e0be3f48799a4783f487da6203245fa5bed239ba7407e1 + languageName: node + linkType: hard + "node-gyp@npm:^9.1.0": version: 9.4.1 resolution: "node-gyp@npm:9.4.1" @@ -16635,6 +16659,16 @@ __metadata: languageName: node linkType: hard +"utf-8-validate@npm:^6.0.5": + version: 6.0.5 + resolution: "utf-8-validate@npm:6.0.5" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10c0/6dc63c513adb001e47a51819072cdd414158430091c49c21d4947ea99f16df5167b671f680df8fb2b6f2ae6a7f30264b4ec111bd3e573720dfe371da1ab99a81 + languageName: node + linkType: hard + "utf8-byte-length@npm:^1.0.1": version: 1.0.5 resolution: "utf8-byte-length@npm:1.0.5" @@ -17069,7 +17103,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.14.1, ws@npm:^8.18.0": +"ws@npm:^8.13.0, ws@npm:^8.14.1, ws@npm:^8.18.0, ws@npm:^8.18.1": version: 8.18.1 resolution: "ws@npm:8.18.1" peerDependencies: From 1c89e823ee5eccc5a7b7f1ade8a7524296f29864 Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Sat, 12 Apr 2025 19:11:32 +0800 Subject: [PATCH 52/59] Delete src/renderer/src/i18n/locales/zh-cn.json.bak --- src/renderer/src/i18n/locales/zh-cn.json.bak | 1576 ------------------ 1 file changed, 1576 deletions(-) delete mode 100644 src/renderer/src/i18n/locales/zh-cn.json.bak diff --git a/src/renderer/src/i18n/locales/zh-cn.json.bak b/src/renderer/src/i18n/locales/zh-cn.json.bak deleted file mode 100644 index 396bed9daf..0000000000 --- a/src/renderer/src/i18n/locales/zh-cn.json.bak +++ /dev/null @@ -1,1576 +0,0 @@ -{ - "translation": { - "voice_call": { - "title": "语音通话", - "start": "开始语音通话", - "end": "结束通话", - "mute": "静音", - "unmute": "取消静音", - "pause": "暂停", - "resume": "继续", - "you": "您", - "ai": "AI", - "press_to_talk": "长按说话", - "release_to_send": "松开发送", - "initialization_failed": "初始化语音通话失败", - "error": "语音通话出错", - "initializing": "正在初始化语音通话...", - "ready": "语音通话已就绪", - "shortcut_key_setting": "语音识别快捷键设置", - "press_any_key": "请按任意键...", - "save": "保存", - "cancel": "取消", - "shortcut_key_tip": "按下此快捷键开始录音,松开快捷键结束录音并发送" - }, - "agents": { - "add.button": "添加到助手", - "add.knowledge_base": "知识库", - "add.knowledge_base.placeholder": "选择知识库", - "add.name": "名称", - "add.name.placeholder": "输入名称", - "add.prompt": "提示词", - "add.prompt.placeholder": "输入提示词", - "add.title": "创建智能体", - "delete.popup.content": "确定要删除此智能体吗?", - "edit.message.add.title": "添加", - "edit.message.assistant.placeholder": "输入助手消息", - "edit.message.assistant.title": "助手", - "edit.message.empty.content": "会话输入内容不能为空", - "edit.message.group.title": "消息组", - "edit.message.title": "预设消息", - "edit.message.user.placeholder": "输入用户消息", - "edit.message.user.title": "用户", - "edit.model.select.title": "选择模型", - "edit.settings.hide_preset_messages": "隐藏预设消息", - "edit.title": "编辑智能体", - "manage.title": "管理智能体", - "my_agents": "我的智能体", - "search.no_results": "没有找到相关智能体", - "sorting.title": "排序", - "tag.agent": "智能体", - "tag.default": "默认", - "tag.new": "新建", - "tag.system": "系统", - "title": "智能体" - }, - "assistants": { - "title": "助手", - "abbr": "助手", - "settings.title": "助手设置", - "clear.content": "清空话题会删除助手下所有话题和文件,确定要继续吗?", - "clear.title": "清空话题", - "copy.title": "复制助手", - "delete.content": "删除助手会删除所有该助手下的话题和文件,确定要继续吗?", - "delete.title": "删除助手", - "edit.title": "编辑助手", - "save.success": "保存成功", - "save.title": "保存到智能体", - "search": "搜索助手", - "settings.mcp": "MCP 服务器", - "settings.mcp.enableFirst": "请先在 MCP 设置中启用此服务器", - "settings.mcp.title": "MCP 设置", - "settings.mcp.noServersAvailable": "无可用 MCP 服务器。请在设置中添加服务器", - "settings.mcp.description": "默认启用的 MCP 服务器", - "settings.default_model": "默认模型", - "settings.knowledge_base": "知识库设置", - "settings.model": "模型设置", - "settings.preset_messages": "预设消息", - "settings.prompt": "提示词设置", - "settings.reasoning_effort": "思维链长度", - "settings.reasoning_effort.high": "长", - "settings.reasoning_effort.low": "短", - "settings.reasoning_effort.medium": "中", - "settings.reasoning_effort.off": "关", - "settings.reasoning_effort.tip": "仅支持 OpenAI o-series、Anthropic、Grok 推理模型", - "settings.more": "助手设置" - }, - "auth": { - "error": "自动获取密钥失败,请手动获取", - "get_key": "获取", - "get_key_success": "自动获取密钥成功", - "login": "登录", - "oauth_button": "使用{{provider}}登录" - }, - "backup": { - "confirm": "确定要备份数据吗?", - "confirm.button": "选择备份位置", - "content": "备份全部数据,包括聊天记录、设置、知识库等所有数据。请注意,备份过程可能需要一些时间,感谢您的耐心等待。", - "progress": { - "completed": "备份完成", - "compressing": "压缩文件...", - "copying_files": "复制文件... {{progress}}%", - "preparing": "准备备份...", - "title": "备份进度", - "writing_data": "写入数据..." - }, - "title": "数据备份" - }, - "button": { - "add": "添加", - "added": "已添加", - "collapse": "收起", - "manage": "管理", - "select_model": "选择模型", - "show.all": "显示全部", - "update_available": "有可用更新" - }, - "chat": { - "add.assistant.title": "添加助手", - "artifacts.button.download": "下载", - "artifacts.button.openExternal": "外部浏览器打开", - "artifacts.button.preview": "预览", - "artifacts.preview.openExternal.error.content": "外部浏览器打开出错", - "assistant.search.placeholder": "搜索", - "deeply_thought": "已深度思考(用时 {{secounds}} 秒)", - "default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。", - "default.name": "默认助手", - "default.topic.name": "默认话题", - "tts": { - "play": "播放语音", - "stop": "停止播放", - "speak": "播放语音", - "stop_global": "停止所有语音播放", - "stopped": "已停止语音播放" - }, - "history": { - "assistant_node": "助手", - "click_to_navigate": "点击跳转到对应消息", - "coming_soon": "聊天工作流图表即将上线", - "no_messages": "没有找到消息", - "start_conversation": "开始对话以查看聊天流程图", - "title": "聊天历史", - "user_node": "用户", - "view_full_content": "查看完整内容" - }, - "input.auto_resize": "自动调整高度", - "input.clear": "清空消息 {{Command}}", - "input.clear.content": "确定要清除当前会话所有消息吗?", - "input.clear.title": "清空消息", - "input.collapse": "收起", - "input.context_count.tip": "上下文数 / 最大上下文数", - "input.estimated_tokens.tip": "预估 token 数", - "input.expand": "展开", - "input.file_not_supported": "模型不支持此文件类型", - "input.generate_image": "生成图片", - "input.generate_image_not_supported": "模型不支持生成图片", - "input.knowledge_base": "知识库", - "input.new.context": "清除上下文 {{Command}}", - "input.new_topic": "新话题 {{Command}}", - "input.pause": "暂停", - "input.placeholder": "在这里输入消息...", - "input.translating": "翻译中...", - "input.send": "发送", - "input.settings": "设置", - "input.topics": " 话题 ", - "input.translate": "翻译成{{target_language}}", - "input.upload": "上传图片或文档", - "input.upload.upload_from_local": "上传本地文件...", - "input.upload.document": "上传文档(模型不支持图片)", - "input.web_search": "开启网络搜索", - "input.web_search.button.ok": "去设置", - "input.web_search.enable": "开启网络搜索", - "input.web_search.enable_content": "需要先在设置中检查网络搜索连通性", - "message.new.branch": "分支", - "message.new.branch.created": "新分支已创建", - "message.new.context": "清除上下文", - "message.quote": "引用", - "message.regenerate.model": "切换模型", - "message.useful": "有用", - "navigation": { - "first": "已经是第一条消息", - "history": "聊天历史", - "last": "已经是最后一条消息", - "next": "下一条消息", - "prev": "上一条消息", - "top": "回到顶部", - "bottom": "回到底部", - "close": "关闭" - }, - "resend": "重新发送", - "save": "保存", - "settings.code_collapsible": "代码块可折叠", - "settings.code_wrappable": "代码块可换行", - "settings.code_cacheable": "代码块缓存", - "settings.code_cacheable.tip": "缓存代码块可以减少长代码块的渲染时间,但会增加内存占用", - "settings.code_cache_max_size": "缓存上限", - "settings.code_cache_max_size.tip": "允许缓存的字符数上限(千字符),按照高亮后的代码计算。高亮后的代码长度相比于纯文本会长很多。", - "settings.code_cache_ttl": "缓存期限", - "settings.code_cache_ttl.tip": "缓存过期时间(分钟)", - "settings.code_cache_threshold": "缓存阈值", - "settings.code_cache_threshold.tip": "允许缓存的最小代码长度(千字符),超过阈值的代码块才会被缓存", - "settings.context_count": "上下文数", - "settings.context_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10", - "settings.max": "不限", - "settings.max_tokens": "开启消息长度限制", - "settings.max_tokens.confirm": "开启消息长度限制", - "settings.max_tokens.confirm_content": "开启消息长度限制后,单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错", - "settings.max_tokens.tip": "单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错", - "settings.reset": "重置", - "settings.set_as_default": "应用到默认助手", - "settings.show_line_numbers": "代码显示行号", - "settings.temperature": "模型温度", - "settings.temperature.tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7", - "settings.thought_auto_collapse": "思考内容自动折叠", - "settings.thought_auto_collapse.tip": "思考结束后思考内容自动折叠", - "settings.top_p": "Top-P", - "settings.top_p.tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化", - "suggestions.title": "建议的问题", - "thinking": "思考中", - "topics.auto_rename": "生成话题名", - "topics.clear.title": "清空消息", - "topics.copy.image": "复制为图片", - "topics.copy.md": "复制为 Markdown", - "topics.copy.title": "复制", - "topics.delete.shortcut": "按住 {{key}} 可直接删除", - "topics.edit.placeholder": "输入新名称", - "topics.edit.title": "编辑话题名", - "topics.export.image": "导出为图片", - "topics.export.joplin": "导出到 Joplin", - "topics.export.md": "导出为 Markdown", - "topics.export.md.reason": "导出为 Markdown (包含思考)", - "topics.export.notion": "导出到 Notion", - "topics.export.obsidian": "导出到 Obsidian", - "topics.export.obsidian_vault": "保管库", - "topics.export.obsidian_vault_placeholder": "请选择保管库名称", - "topics.export.obsidian_path": "路径", - "topics.export.obsidian_path_placeholder": "请选择路径", - "topics.export.obsidian_atributes": "配置笔记属性", - "topics.export.obsidian_btn": "确定", - "topics.export.obsidian_created": "创建时间", - "topics.export.obsidian_created_placeholder": "请选择创建时间", - "topics.export.obsidian_export_failed": "导出到Obsidian失败", - "topics.export.obsidian_export_success": "导出到Obsidian成功", - "topics.export.obsidian_operate": "处理方式", - "topics.export.obsidian_operate_append": "追加", - "topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆盖)", - "topics.export.obsidian_operate_placeholder": "请选择处理方式", - "topics.export.obsidian_operate_prepend": "前置", - "topics.export.obsidian_source": "来源", - "topics.export.obsidian_source_placeholder": "请输入来源", - "topics.export.obsidian_tags": "标签", - "topics.export.obsidian_tags_placeholder": "请输入标签,多个标签用英文逗号分隔", - "topics.export.obsidian_title": "标题", - "topics.export.obsidian_title_placeholder": "请输入标题", - "topics.export.obsidian_title_required": "标题不能为空", - "topics.export.obsidian_no_vaults": "未找到Obsidian保管库", - "topics.export.obsidian_loading": "加载中...", - "topics.export.obsidian_fetch_error": "获取Obsidian保管库失败", - "topics.export.obsidian_fetch_folders_error": "获取文件夹结构失败", - "topics.export.obsidian_no_vault_selected": "请先选择一个保管库", - "topics.export.obsidian_select_vault_first": "请先选择保管库", - "topics.export.obsidian_root_directory": "根目录", - "topics.export.title": "导出", - "topics.export.word": "导出为 Word", - "topics.export.yuque": "导出到语雀", - "topics.list": "话题列表", - "topics.move_to": "移动到", - "topics.new": "开始新对话", - "topics.pinned": "固定话题", - "topics.prompt": "话题提示词", - "topics.prompt.edit.title": "编辑话题提示词", - "topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词", - "topics.title": "话题", - "topics.unpinned": "取消固定", - "translate": "翻译", - "topics.export.siyuan": "导出到思源笔记", - "topics.export.wait_for_title_naming": "正在生成标题...", - "topics.export.title_naming_success": "标题生成成功", - "topics.export.title_naming_failed": "标题生成失败,使用默认标题" - }, - "code_block": { - "collapse": "收起", - "disable_wrap": "取消换行", - "enable_wrap": "换行", - "expand": "展开" - }, - "common": { - "add": "添加", - "advanced_settings": "高级设置", - "and": "和", - "assistant": "智能体", - "avatar": "头像", - "back": "返回", - "cancel": "取消", - "chat": "聊天", - "clear": "清除", - "close": "关闭", - "confirm": "确认", - "copied": "已复制", - "copy": "复制", - "cut": "剪切", - "default": "默认", - "delete": "删除", - "description": "描述", - "docs": "文档", - "download": "下载", - "duplicate": "复制", - "edit": "编辑", - "expand": "展开", - "collapse": "折叠", - "footnote": "引用内容", - "footnotes": "引用内容", - "fullscreen": "已进入全屏模式,按 F11 退出", - "knowledge_base": "知识库", - "language": "语言", - "model": "模型", - "models": "模型", - "more": "更多", - "name": "名称", - "paste": "粘贴", - "prompt": "提示词", - "provider": "提供商", - "regenerate": "重新生成", - "rename": "重命名", - "reset": "重置", - "save": "保存", - "search": "搜索", - "select": "选择", - "topics": "话题", - "warning": "警告", - "you": "用户", - "reasoning_content": "已深度思考", - "sort": { - "pinyin": "按拼音排序", - "pinyin.asc": "按拼音升序", - "pinyin.desc": "按拼音降序" - } - }, - "docs": { - "title": "帮助文档" - }, - "error": { - "backup.file_format": "备份文件格式错误", - "chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥", - "http": { - "400": "请求错误,请检查请求参数是否正确。如果修改了模型设置,请重置到默认设置", - "401": "身份验证失败,请检查 API 密钥是否正确", - "403": "禁止访问,请翻译具体报错信息查看原因,或联系服务商询问被禁止原因", - "404": "模型不存在或者请求路径错误", - "429": "请求速率超过限制,请稍后再试", - "500": "服务器错误,请稍后再试", - "502": "网关错误,请稍后再试", - "503": "服务不可用,请稍后再试", - "504": "网关超时,请稍后再试" - }, - "model.exists": "模型已存在", - "no_api_key": "API 密钥未配置", - "provider_disabled": "模型提供商未启用", - "render": { - "description": "渲染公式失败,请检查公式格式是否正确", - "title": "渲染错误" - }, - "user_message_not_found": "无法找到原始用户消息", - "unknown": "未知错误" - }, - "export": { - "assistant": "助手", - "attached_files": "附件", - "conversation_details": "会话详情", - "conversation_history": "会话历史", - "created": "创建时间", - "last_updated": "最后更新", - "messages": "消息数", - "user": "用户" - }, - "files": { - "actions": "操作", - "all": "所有文件", - "count": "个文件", - "created_at": "创建时间", - "delete": "删除", - "delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?", - "delete.paintings.warning": "绘图中包含该图片,暂时无法删除", - "delete.title": "删除文件", - "document": "文档", - "edit": "编辑", - "file": "文件", - "image": "图片", - "name": "文件名", - "open": "打开", - "size": "大小", - "text": "文本", - "title": "文件", - "type": "类型" - }, - "gpustack": { - "keep_alive_time.description": "模型在内存中保持的时间(默认:5分钟)", - "keep_alive_time.placeholder": "分钟", - "keep_alive_time.title": "保持活跃时间", - "title": "GPUStack" - }, - "history": { - "continue_chat": "继续聊天", - "locate.message": "定位到消息", - "search.messages": "搜索所有消息", - "search.placeholder": "搜索话题或消息...", - "search.topics.empty": "没有找到相关话题, 点击回车键搜索所有消息", - "title": "话题搜索" - }, - "knowledge": { - "add": { - "title": "添加知识库" - }, - "add_directory": "添加目录", - "add_file": "添加文件", - "add_note": "添加笔记", - "add_sitemap": "站点地图", - "add_url": "添加网址", - "cancel_index": "取消索引", - "chunk_overlap": "重叠大小", - "chunk_overlap_placeholder": "默认值(不建议修改)", - "chunk_overlap_tooltip": "相邻文本块之间重复的内容量,确保分段后的文本块之间仍然有上下文联系,提升模型处理长文本的整体效果", - "chunk_size": "分段大小", - "chunk_size_change_warning": "分段大小和重叠大小修改只针对新添加的内容有效", - "chunk_size_placeholder": "默认值(不建议修改)", - "chunk_size_too_large": "分段大小不能超过模型上下文限制({{max_context}})", - "chunk_size_tooltip": "将文档切割分段,每段的大小,不能超过模型上下文限制", - "clear_selection": "清除选择", - "delete": "删除", - "delete_confirm": "确定要删除此知识库吗?", - "directories": "目录", - "directory_placeholder": "请输入目录路径", - "document_count": "请求文档片段数量", - "document_count_default": "默认", - "document_count_help": "请求文档片段数量越多,附带的信息越多,但需要消耗的 Token 也越多", - "drag_file": "拖拽文件到这里", - "edit_remark": "修改备注", - "edit_remark_placeholder": "请输入备注内容", - "empty": "暂无知识库", - "file_hint": "支持 {{file_types}} 格式", - "index_all": "索引全部", - "index_cancelled": "索引已取消", - "index_started": "索引开始", - "invalid_url": "无效的网址", - "model_info": "模型信息", - "no_bases": "暂无知识库", - "no_match": "未匹配到知识库内容", - "no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库", - "not_set": "未设置", - "not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库", - "notes": "笔记", - "notes_placeholder": "输入此知识库的附加信息或上下文...", - "rename": "重命名", - "search": "搜索知识库", - "search_placeholder": "输入查询内容", - "settings": "知识库设置", - "sitemap_placeholder": "请输入站点地图 URL", - "sitemaps": "网站", - "source": "来源", - "status": "状态", - "status_completed": "已完成", - "status_failed": "失败", - "status_new": "已添加", - "status_pending": "等待中", - "status_processing": "处理中", - "threshold": "匹配度阈值", - "threshold_placeholder": "未设置", - "threshold_too_large_or_small": "阈值不能大于1或小于0", - "threshold_tooltip": "用于衡量用户问题与知识库内容之间的相关性(0-1)", - "title": "知识库", - "topN": "返回结果数量", - "topN__too_large_or_small": "返回结果数量不能大于100或小于1", - "topN_placeholder": "未设置", - "topN_tooltip": "返回的匹配结果数量,数值越大,匹配结果越多,但消耗的 Token 也越多", - "url_added": "网址已添加", - "url_placeholder": "请输入网址, 多个网址用回车分隔", - "urls": "网址" - }, - "languages": { - "arabic": "阿拉伯文", - "chinese": "简体中文", - "chinese-traditional": "繁体中文", - "english": "英文", - "french": "法文", - "german": "德文", - "italian": "意大利文", - "japanese": "日文", - "korean": "韩文", - "portuguese": "葡萄牙文", - "russian": "俄文", - "spanish": "西班牙文" - }, - "lmstudio": { - "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)", - "keep_alive_time.placeholder": "分钟", - "keep_alive_time.title": "保持活跃时间", - "title": "LM Studio" - }, - "mermaid": { - "download": { - "png": "下载 PNG", - "svg": "下载 SVG" - }, - "resize": { - "zoom-in": "放大", - "zoom-out": "缩小" - }, - "tabs": { - "preview": "预览", - "source": "源码" - }, - "title": "Mermaid 图表" - }, - "message": { - "api.check.model.title": "请选择要检测的模型", - "api.connection.failed": "连接失败", - "api.connection.success": "连接成功", - "assistant.added.content": "智能体添加成功", - "attachments": { - "pasted_image": "剪切板图片", - "pasted_text": "剪切板文件" - }, - "backup.failed": "备份失败", - "backup.start.success": "开始备份", - "backup.success": "备份成功", - "chat.completion.paused": "会话已停止", - "citations": "引用内容", - "copied": "已复制", - "copy.failed": "复制失败", - "copy.success": "复制成功", - "error.chunk_overlap_too_large": "分段重叠不能大于分段大小", - "error.dimension_too_large": "内容尺寸过大", - "error.enter.api.host": "请输入您的 API 地址", - "error.enter.api.key": "请输入您的 API 密钥", - "error.enter.model": "请选择一个模型", - "error.enter.name": "请输入知识库名称", - "error.get_embedding_dimensions": "获取嵌入维度失败", - "error.invalid.api.host": "无效的 API 地址", - "error.invalid.api.key": "无效的 API 密钥", - "error.invalid.enter.model": "请选择一个模型", - "error.invalid.proxy.url": "无效的代理地址", - "error.invalid.webdav": "无效的 WebDAV 设置", - "error.joplin.export": "导出 Joplin 失败,请保持 Joplin 已运行并检查连接状态或检查配置", - "error.joplin.no_config": "未配置 Joplin 授权令牌 或 URL", - "error.invalid.nutstore": "无效的坚果云设置", - "error.invalid.nutstore_token": "无效的坚果云 Token", - "error.markdown.export.preconf": "导出Markdown文件到预先设定的路径失败", - "error.markdown.export.specified": "导出Markdown文件失败", - "error.notion.export": "导出 Notion 错误,请检查连接状态并对照文档检查配置", - "error.notion.no_api_key": "未配置 Notion API Key 或 Notion Database ID", - "error.yuque.export": "导出语雀错误,请检查连接状态并对照文档检查配置", - "error.yuque.no_config": "未配置语雀 Token 或 知识库 URL", - "group.delete.content": "删除分组消息会删除用户提问和所有助手的回答", - "group.delete.title": "删除分组消息", - "ignore.knowledge.base": "联网模式开启,忽略知识库", - "info.notion.block_reach_limit": "对话过长,正在分页导出到Notion", - "loading.notion.exporting_progress": "正在导出到Notion ({{current}}/{{total}})...", - "loading.notion.preparing": "正在准备导出到Notion...", - "mention.title": "切换模型回答", - "message.code_style": "代码风格", - "message.delete.content": "确定要删除此消息吗?", - "message.delete.title": "删除消息", - "message.multi_model_style": "多模型回答样式", - "message.multi_model_style.fold": "标签模式", - "message.multi_model_style.fold.compress": "切换到紧凑排列", - "message.multi_model_style.fold.expand": "切换到展开排列", - "message.multi_model_style.grid": "卡片布局", - "message.multi_model_style.horizontal": "横向排列", - "message.multi_model_style.vertical": "纵向堆叠", - "message.style": "消息样式", - "message.style.bubble": "气泡", - "message.style.plain": "简洁", - "regenerate.confirm": "重新生成会覆盖当前消息", - "reset.confirm.content": "确定要重置所有数据吗?", - "reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?", - "reset.double.confirm.title": "数据丢失!!!", - "restore.failed": "恢复失败", - "restore.success": "恢复成功", - "save.success.title": "保存成功", - "searching": "正在联网搜索...", - "success.joplin.export": "成功导出到 Joplin", - "success.markdown.export.preconf": "成功导出 Markdown 文件到预先设定的路径", - "success.markdown.export.specified": "成功导出 Markdown 文件", - "success.notion.export": "成功导出到 Notion", - "success.yuque.export": "成功导出到语雀", - "switch.disabled": "请等待当前回复完成后操作", - "tools": { - "completed": "已完成", - "invoking": "调用中" - }, - "topic.added": "话题添加成功", - "upgrade.success.button": "重启", - "upgrade.success.content": "重启用以完成升级", - "upgrade.success.title": "升级成功", - "warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!", - "warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试", - "error.siyuan.export": "导出思源笔记失败,请检查连接状态并对照文档检查配置", - "error.siyuan.no_config": "未配置思源笔记API地址或令牌", - "success.siyuan.export": "导出到思源笔记成功", - "warn.yuque.exporting": "正在导出语雀, 请勿重复请求导出!", - "warn.siyuan.exporting": "正在导出到思源笔记,请勿重复请求导出!" - }, - "minapp": { - "popup": { - "refresh": "刷新", - "close": "关闭小程序", - "minimize": "最小化小程序", - "devtools": "开发者工具", - "openExternal": "在浏览器中打开", - "rightclick_copyurl": "右键复制URL" - }, - "sidebar.add.title": "添加到侧边栏", - "sidebar.remove.title": "从侧边栏移除", - "sidebar.close.title": "关闭", - "sidebar.closeall.title": "全部关闭", - "sidebar.hide.title": "隐藏小程序", - "title": "小程序" - }, - "miniwindow": { - "clipboard": { - "empty": "剪贴板为空" - }, - "feature": { - "chat": "回答此问题", - "explanation": "解释说明", - "summary": "内容总结", - "translate": "文本翻译" - }, - "footer": { - "copy_last_message": "按 C 键复制", - "backspace_clear": "按 Backspace 清空", - "esc": "按 ESC {{action}}", - "esc_back": "返回", - "esc_close": "关闭" - }, - "input": { - "placeholder": { - "empty": "询问 {{model}} 获取帮助...", - "title": "你想对下方文字做什么" - } - }, - "tooltip": { - "pin": "窗口置顶" - } - }, - "models": { - "add_parameter": "添加参数", - "all": "全部", - "custom_parameters": "自定义参数", - "dimensions": "{{dimensions}} 维", - "edit": "编辑模型", - "embedding": "嵌入", - "embedding_model": "嵌入模型", - "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", - "function_calling": "函数调用", - "no_matches": "无可用模型", - "parameter_name": "参数名称", - "parameter_type": { - "boolean": "布尔值", - "json": "JSON", - "number": "数字", - "string": "文本" - }, - "pinned": "已固定", - "rerank_model": "重排模型", - "rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})", - "rerank_model_tooltip": "在设置->模型服务中点击管理按钮添加", - "search": "搜索模型...", - "stream_output": "流式输出", - "type": { - "embedding": "嵌入", - "free": "免费", - "function_calling": "工具", - "reasoning": "推理", - "rerank": "重排", - "select": "选择模型类型", - "text": "文本", - "vision": "视觉", - "websearch": "联网" - } - }, - "navbar": { - "expand": "伸缩对话框", - "hide_sidebar": "隐藏侧边栏", - "show_sidebar": "显示侧边栏" - }, - "ollama": { - "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)", - "keep_alive_time.placeholder": "分钟", - "keep_alive_time.title": "保持活跃时间", - "title": "Ollama" - }, - "paintings": { - "button.delete.image": "删除图片", - "button.delete.image.confirm": "确定要删除此图片吗?", - "button.new.image": "新建图片", - "guidance_scale": "引导比例", - "guidance_scale_tip": "无分类器指导。控制模型在寻找相关图像时对提示词的遵循程度", - "image.size": "图片尺寸", - "inference_steps": "推理步数", - "inference_steps_tip": "要执行的推理步数。步数越多,质量越高但耗时越长", - "negative_prompt": "反向提示词", - "negative_prompt_tip": "描述你不想在图片中出现的内容", - "number_images": "生成数量", - "number_images_tip": "一次生成的图片数量 (1-4)", - "prompt_enhancement": "提示词增强", - "prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本", - "prompt_placeholder": "描述你想创建的图片,例如:一个宁静的湖泊,夕阳西下,远处是群山", - "regenerate.confirm": "这将覆盖已生成的图片,是否继续?", - "seed": "随机种子", - "seed_tip": "相同的种子和提示词可以生成相似的图片", - "title": "图片" - }, - "plantuml": { - "download": { - "failed": "下载失败,请检查网络", - "png": "下载 PNG", - "svg": "下载 SVG" - }, - "tabs": { - "preview": "预览", - "source": "源码" - }, - "title": "PlantUML 图表" - }, - "prompts": { - "explanation": "帮我解释一下这个概念", - "summarize": "帮我总结一下这段话", - "title": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号" - }, - "provider": { - "aihubmix": "AiHubMix", - "alayanew": "Alaya NeW", - "anthropic": "Anthropic", - "azure-openai": "Azure OpenAI", - "baichuan": "百川", - "baidu-cloud": "百度云千帆", - "copilot": "GitHub Copilot", - "dashscope": "阿里云百炼", - "deepseek": "深度求索", - "dmxapi": "DMXAPI", - "doubao": "火山引擎", - "fireworks": "Fireworks", - "gemini": "Gemini", - "gitee-ai": "Gitee AI", - "github": "GitHub Models", - "gpustack": "GPUStack", - "grok": "Grok", - "groq": "Groq", - "hunyuan": "腾讯混元", - "hyperbolic": "Hyperbolic", - "infini": "无问芯穹", - "jina": "Jina", - "lmstudio": "LM Studio", - "minimax": "MiniMax", - "mistral": "Mistral", - "modelscope": "ModelScope 魔搭", - "moonshot": "月之暗面", - "nvidia": "英伟达", - "o3": "O3", - "ocoolai": "ocoolAI", - "ollama": "Ollama", - "openai": "OpenAI", - "openrouter": "OpenRouter", - "perplexity": "Perplexity", - "ppio": "PPIO 派欧云", - "qwenlm": "QwenLM", - "silicon": "硅基流动", - "stepfun": "阶跃星辰", - "tencent-cloud-ti": "腾讯云TI", - "together": "Together", - "xirang": "天翼云息壤", - "yi": "零一万物", - "zhinao": "360智脑", - "zhipu": "智谱AI", - "voyageai": "Voyage AI", - "qiniu": "七牛云" - }, - "restore": { - "confirm": "确定要恢复数据吗?", - "confirm.button": "选择备份文件", - "content": "恢复操作将使用备份数据覆盖当前所有应用数据。请注意,恢复过程可能需要一些时间,感谢您的耐心等待。", - "progress": { - "completed": "恢复完成", - "copying_files": "复制文件... {{progress}}%", - "extracting": "解压备份...", - "preparing": "准备恢复...", - "reading_data": "读取数据...", - "title": "恢复进度" - }, - "title": "数据恢复" - }, - "settings": { - "about": "关于我们", - "about.checkingUpdate": "正在检查更新...", - "about.checkUpdate": "检查更新", - "about.checkUpdate.available": "立即更新", - "about.contact.button": "邮件", - "about.contact.title": "邮件联系", - "about.description": "一款为创造者而生的 AI 助手", - "about.downloading": "正在下载更新...", - "about.feedback.button": "反馈", - "about.feedback.title": "意见反馈", - "about.license.button": "查看", - "about.license.title": "许可证", - "about.releases.button": "查看", - "about.releases.title": "更新日志", - "about.social.title": "社交账号", - "about.title": "关于我们", - "about.updateAvailable": "发现新版本 {{version}}", - "about.updateError": "更新出错", - "about.updateNotAvailable": "你的软件已是最新版本", - "about.website.button": "查看", - "about.website.title": "官方网站", - "advanced.auto_switch_to_topics": "自动切换到话题", - "advanced.title": "高级设置", - "assistant": "默认助手", - "assistant.model_params": "模型参数", - "assistant.show.icon": "显示模型图标", - "assistant.title": "默认助手", - "data": { - "app_data": "应用数据", - "app_knowledge": "知识库文件", - "app_knowledge.button.delete": "删除文件", - "app_knowledge.remove_all": "删除知识库文件", - "app_knowledge.remove_all_confirm": "删除知识库文件可以减少存储空间占用,但不会删除知识库向量化数据,删除之后将无法打开源文件,是否删除?", - "app_knowledge.remove_all_success": "文件删除成功", - "app_logs": "应用日志", - "clear_cache": { - "button": "清除缓存", - "confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?", - "error": "清除缓存失败", - "success": "缓存清除成功", - "title": "清除缓存" - }, - "data.title": "数据目录", - "divider.basic": "基础数据设置", - "divider.cloud_storage": "云备份设置", - "divider.export_settings": "导出设置", - "divider.third_party": "第三方连接", - "hour_interval_one": "{{count}} 小时", - "hour_interval_other": "{{count}} 小时", - "export_menu": { - "title": "导出菜单设置", - "image": "导出为图片", - "markdown": "导出为Markdown", - "markdown_reason": "导出为Markdown(包含思考)", - "notion": "导出到Notion", - "yuque": "导出到语雀", - "obsidian": "导出到Obsidian", - "siyuan": "导出到思源笔记", - "joplin": "导出到Joplin", - "docx": "导出为Word" - }, - "joplin": { - "check": { - "button": "检查", - "empty_token": "请先输入 Joplin 授权令牌", - "empty_url": "请先输入 Joplin 剪裁服务监听 URL", - "fail": "Joplin 连接验证失败", - "success": "Joplin 连接验证成功" - }, - "help": "在 Joplin 选项中,启用网页剪裁服务(无需安装浏览器插件),确认端口号,并复制授权令牌", - "title": "Joplin 配置", - "token": "Joplin 授权令牌", - "token_placeholder": "请输入 Joplin 授权令牌", - "url": "Joplin 剪裁服务监听 URL", - "url_placeholder": "http://127.0.0.1:41184/" - }, - "markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。", - "markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式", - "markdown_export.help": "若填入,则每次导出时将自动保存到该路径;否则,将弹出保存对话框", - "markdown_export.path": "默认导出路径", - "markdown_export.path_placeholder": "导出路径", - "markdown_export.select": "选择", - "markdown_export.title": "Markdown 导出", - "message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题", - "message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式。", - "minute_interval_one": "{{count}} 分钟", - "minute_interval_other": "{{count}} 分钟", - "notion.api_key": "Notion 密钥", - "notion.api_key_placeholder": "请输入Notion 密钥", - "notion.auto_split": "导出对话时自动分页", - "notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion", - "notion.check": { - "button": "检查", - "empty_api_key": "未配置 API key", - "empty_database_id": "未配置 Database ID", - "error": "连接异常,请检查网络及 API key 和 Database ID 是否正确", - "fail": "连接失败,请检查网络及 API key 和 Database ID 是否正确", - "success": "连接成功" - }, - "notion.database_id": "Notion 数据库 ID", - "notion.database_id_placeholder": "请输入Notion 数据库 ID", - "notion.help": "Notion 配置文档", - "notion.page_name_key": "页面标题字段名", - "notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name", - "notion.split_size": "自动分页大小", - "notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90", - "notion.split_size_placeholder": "请输入每页块数限制(默认90)", - "notion.title": "Notion 配置", - "title": "数据设置", - "webdav": { - "autoSync": "自动备份", - "autoSync.off": "关闭", - "backup.button": "备份到 WebDAV", - "backup.modal.filename.placeholder": "请输入备份文件名", - "backup.modal.title": "备份到 WebDAV", - "host": "WebDAV 地址", - "host.placeholder": "http://localhost:8080", - "hour_interval_one": "{{count}} 小时", - "hour_interval_other": "{{count}} 小时", - "lastSync": "上次备份时间", - "minute_interval_one": "{{count}} 分钟", - "minute_interval_other": "{{count}} 分钟", - "noSync": "等待下次备份", - "password": "WebDAV 密码", - "path": "WebDAV 路径", - "path.placeholder": "/backup", - "restore.button": "从 WebDAV 恢复", - "restore.confirm.content": "从 WebDAV 恢复将会覆盖当前数据,是否继续?", - "restore.confirm.title": "确认恢复", - "restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?", - "restore.modal.select.placeholder": "请选择要恢复的备份文件", - "restore.modal.title": "从 WebDAV 恢复", - "restore.title": "从 WebDAV 恢复", - "syncError": "备份错误", - "syncStatus": "备份状态", - "title": "WebDAV", - "user": "WebDAV 用户名" - }, - "yuque": { - "check": { - "button": "检查", - "empty_repo_url": "请先输入知识库URL", - "empty_token": "请先输入语雀Token", - "fail": "语雀连接验证失败", - "success": "语雀连接验证成功" - }, - "help": "获取语雀 Token", - "repo_url": "知识库 URL", - "repo_url_placeholder": "https://www.yuque.com/username/xxx", - "title": "语雀配置", - "token": "语雀 Token", - "token_placeholder": "请输入语雀Token" - }, - "obsidian": { - "title": "Obsidian 配置", - "default_vault": "默认 Obsidian 仓库", - "default_vault_placeholder": "请选择默认 Obsidian 仓库", - "default_vault_loading": "正在获取 Obsidian 仓库...", - "default_vault_no_vaults": "未找到 Obsidian 仓库", - "default_vault_fetch_error": "获取 Obsidian 仓库失败", - "default_vault_export_failed": "导出失败" - }, - "siyuan": { - "title": "思源笔记配置", - "api_url": "API地址", - "api_url_placeholder": "例如:http://127.0.0.1:6806", - "token": "API令牌", - "token.help": "在思源笔记->设置->关于中获取", - "token_placeholder": "请输入思源笔记令牌", - "box_id": "笔记本ID", - "box_id_placeholder": "请输入笔记本ID", - "root_path": "文档根路径", - "root_path_placeholder": "例如:/CherryStudio", - "check": { - "title": "连接检查", - "button": "检查", - "empty_config": "请填写API地址和令牌", - "success": "连接成功", - "fail": "连接失败,请检查API地址和令牌", - "error": "连接异常,请检查网络连接" - } - }, - "nutstore": { - "title": "坚果云配置", - "isLogin": "已登录", - "notLogin": "未登录", - "login.button": "登录", - "logout.button": "退出登录", - "logout.title": "确定要退出坚果云登录?", - "logout.content": "退出后将无法备份至坚果云和从坚果云恢复", - "checkConnection.name": "检查连接", - "checkConnection.success": "已连接坚果云", - "checkConnection.fail": "坚果云连接失败", - "username": "坚果云用户名", - "path": "坚果云存储路径", - "path.placeholder": "请输入坚果云的存储路径", - "backup.button": "备份到坚果云", - "restore.button": "从坚果云恢复", - "pathSelector.title": "坚果云存储路径", - "pathSelector.return": "返回", - "pathSelector.currentPath": "当前路径", - "new_folder.button.confirm": "确定", - "new_folder.button.cancel": "取消", - "new_folder.button": "新建文件夹" - } - }, - "display.assistant.title": "助手设置", - "display.custom.css": "自定义 CSS", - "display.custom.css.cherrycss": "从 cherrycss.com 获取", - "display.custom.css.placeholder": "/* 这里写自定义CSS */", - "display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏", - "display.sidebar.disabled": "隐藏的图标", - "display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里", - "display.sidebar.files.icon": "显示文件图标", - "display.sidebar.knowledge.icon": "显示知识图标", - "display.sidebar.minapp.icon": "显示小程序图标", - "display.sidebar.painting.icon": "显示绘画图标", - "display.sidebar.title": "侧边栏设置", - "display.sidebar.translate.icon": "显示翻译图标", - "display.sidebar.visible": "显示的图标", - "display.title": "显示设置", - "display.topic.title": "话题设置", - "miniapps": { - "title": "小程序设置", - "disabled": "隐藏的小程序", - "empty": "把要隐藏的小程序从左侧拖拽到这里", - "visible": "显示的小程序", - "cache_settings": "缓存设置", - "cache_title": "小程序缓存数量", - "cache_description": "设置同时保持活跃状态的小程序最大数量", - "reset_tooltip": "重置为默认值", - "display_title": "小程序显示设置", - "sidebar_title": "侧边栏活跃小程序显示设置", - "sidebar_description": "设置侧边栏是否显示活跃的小程序", - "cache_change_notice": "更改将在打开的小程序增减至设定值后生效" - }, - "font_size.title": "消息字体大小", - "general": "常规设置", - "general.avatar.reset": "重置头像", - "general.backup.button": "备份", - "general.backup.title": "数据备份与恢复", - "general.display.title": "显示设置", - "general.emoji_picker": "表情选择器", - "general.image_upload": "图片上传", - "general.auto_check_update.title": "自动检测更新", - "general.reset.button": "重置", - "general.reset.title": "重置数据", - "general.restore.button": "恢复", - "general.title": "常规设置", - "general.user_name": "用户名", - "general.user_name.placeholder": "请输入用户名", - "general.view_webdav_settings": "查看 WebDAV 设置", - "input.auto_translate_with_space": "快速敲击3次空格翻译", - "input.target_language": "目标语言", - "input.target_language.chinese": "简体中文", - "input.target_language.chinese-traditional": "繁体中文", - "input.target_language.english": "英文", - "input.target_language.japanese": "日文", - "input.target_language.russian": "俄文", - "launch.onboot": "开机自动启动", - "launch.title": "启动", - "launch.totray": "启动时最小化到托盘", - "mcp": { - "actions": "操作", - "active": "启用", - "addError": "添加服务器失败", - "addServer": "添加服务器", - "addSuccess": "服务器添加成功", - "args": "参数", - "argsTooltip": "每个参数占一行", - "baseUrlTooltip": "远程 URL 地址", - "command": "命令", - "sse": "服务器发送事件 (sse)", - "streamableHttp": "可流式传输的HTTP (streamableHttp)", - "stdio": "标准输入/输出 (stdio)", - "inMemory": "内存", - "config_description": "配置模型上下文协议服务器", - "deleteError": "删除服务器失败", - "deleteSuccess": "服务器删除成功", - "dependenciesInstall": "安装依赖项", - "dependenciesInstalling": "正在安装依赖项...", - "description": "描述", - "duplicateName": "已存在同名服务器", - "editJson": "编辑JSON", - "editServer": "编辑服务器", - "env": "环境变量", - "envTooltip": "格式:KEY=value,每行一个", - "findMore": "更多 MCP", - "searchNpx": "搜索 MCP", - "install": "安装", - "installError": "安装依赖项失败", - "installSuccess": "依赖项安装成功", - "jsonFormatError": "JSON格式化错误", - "jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确。", - "jsonSaveError": "保存JSON配置失败", - "jsonSaveSuccess": "JSON配置已保存", - "missingDependencies": "缺失,请安装它以继续", - "name": "名称", - "noServers": "未配置服务器", - "newServer": "MCP 服务器", - "npx_list": { - "actions": "操作", - "desc": "搜索并添加 npm 包作为 MCP 服务", - "description": "描述", - "no_packages": "未找到包", - "npm": "NPM", - "package_name": "包名称", - "scope_placeholder": "输入 npm 作用域 (例如 @your-org)", - "scope_required": "请输入 npm 作用域", - "search": "搜索", - "search_error": "搜索失败", - "title": "NPX 包列表", - "usage": "用法", - "version": "版本" - }, - "errors": { - "32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整" - }, - "serverPlural": "服务器", - "serverSingular": "服务器", - "title": "MCP 服务器", - "startError": "启动失败", - "type": "类型", - "updateError": "更新服务器失败", - "updateSuccess": "服务器更新成功", - "url": "URL", - "editMcpJson": "编辑 MCP 配置", - "installHelp": "获取安装帮助", - "tools": { - "inputSchema": "输入参数", - "availableTools": "可用工具", - "noToolsAvailable": "没有可用工具" - }, - "deleteServer": "删除服务器", - "deleteServerConfirm": "确定要删除此服务器吗?", - "registry": "包管理源", - "registryTooltip": "选择用于安装包的源,以解决默认源的网络问题。", - "registryDefault": "默认", - "not_support": "模型不支持", - "user": "用户", - "system": "系统" - }, - "messages.divider": "消息分割线", - "messages.grid_columns": "消息网格展示列数", - "messages.grid_popover_trigger": "网格详情触发", - "messages.grid_popover_trigger.click": "点击显示", - "messages.grid_popover_trigger.hover": "悬停显示", - "messages.input.paste_long_text_as_file": "长文本粘贴为文件", - "messages.input.paste_long_text_threshold": "长文本长度", - "messages.input.send_shortcuts": "发送快捷键", - "messages.input.show_estimated_tokens": "显示预估 Token 数", - "messages.input.title": "输入设置", - "messages.input.enable_quick_triggers": "启用 '/' 和 '@' 触发快捷菜单", - "messages.markdown_rendering_input_message": "Markdown 渲染输入消息", - "messages.math_engine": "数学公式引擎", - "messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens", - "messages.model.title": "模型设置", - "messages.navigation": "对话导航按钮", - "messages.navigation.anchor": "对话锚点", - "messages.navigation.buttons": "上下按钮", - "messages.navigation.none": "不显示", - "messages.title": "消息设置", - "messages.use_serif_font": "使用衬线字体", - "model": "默认模型", - "models.add.add_model": "添加模型", - "models.add.group_name": "分组名称", - "models.add.group_name.placeholder": "例如 ChatGPT", - "models.add.group_name.tooltip": "例如 ChatGPT", - "models.add.model_id": "模型 ID", - "models.add.model_id.placeholder": "必填 例如 gpt-3.5-turbo", - "models.add.model_id.tooltip": "例如 gpt-3.5-turbo", - "models.add.model_name": "模型名称", - "models.add.model_name.placeholder": "例如 GPT-3.5", - "models.check.all": "所有", - "models.check.all_models_passed": "所有模型检查通过", - "models.check.button_caption": "健康检查", - "models.check.disabled": "关闭", - "models.check.enable_concurrent": "并发检查", - "models.check.enabled": "开启", - "models.check.failed": "失败", - "models.check.keys_status_count": "通过:{{count_passed}}个密钥,失败:{{count_failed}}个密钥", - "models.check.model_status_summary": "{{provider}}: {{count_passed}} 个模型完成健康检查(其中 {{count_partial}} 个模型用某些密钥无法访问),{{count_failed}} 个模型完全无法访问。", - "models.check.no_api_keys": "未找到API密钥,请先添加API密钥。", - "models.check.passed": "通过", - "models.check.select_api_key": "选择要使用的API密钥:", - "models.check.single": "单个", - "models.check.start": "开始", - "models.check.title": "模型健康检查", - "models.check.use_all_keys": "使用密钥", - "models.default_assistant_model": "默认助手模型", - "models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型", - "models.empty": "没有模型", - "models.enable_topic_naming": "话题自动重命名", - "models.manage.add_whole_group": "添加整个分组", - "models.manage.remove_whole_group": "移除整个分组", - "models.topic_naming_model": "话题命名模型", - "models.topic_naming_model_description": "自动命名新话题时使用的模型", - "models.topic_naming_model_setting_title": "话题命名模型设置", - "models.topic_naming_prompt": "话题命名提示词", - "models.translate_model": "翻译模型", - "models.translate_model_description": "翻译服务使用的模型", - "models.translate_model_prompt_message": "请输入翻译模型提示词", - "models.translate_model_prompt_title": "翻译模型提示词", - "moresetting": "更多设置", - "moresetting.check.confirm": "确认勾选", - "moresetting.check.warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!", - "moresetting.warn": "风险警告", - "provider": { - "add.name": "提供商名称", - "add.name.placeholder": "例如 OpenAI", - "add.title": "添加提供商", - "add.type": "提供商类型", - "api.url.preview": "预览: {{url}}", - "api.url.reset": "重置", - "api.url.tip": "/结尾忽略v1版本,#结尾强制使用输入地址", - "api_host": "API 地址", - "api_key": "API 密钥", - "api_key.tip": "多个密钥使用逗号分隔", - "api_version": "API 版本", - "charge": "充值", - "check": "检查", - "check_all_keys": "检查所有密钥", - "check_multiple_keys": "检查多个 API 密钥", - "copilot": { - "auth_failed": "Github Copilot 认证失败", - "auth_success": "Github Copilot 认证成功", - "auth_success_title": "认证成功", - "code_failed": "获取 Device Code 失败,请重试", - "code_generated_desc": "请将 Device Code 复制到下面的浏览器链接中", - "code_generated_title": "获取 Device Code", - "confirm_login": "过度使用可能会导致您的 Github 账号遭到封号,请谨慎使用!!!!", - "confirm_title": "风险警告", - "connect": "连接 Github", - "custom_headers": "自定义请求头", - "description": "您的 Github 账号需要订阅 Copilot", - "expand": "展开", - "headers_description": "自定义请求头(json格式)", - "invalid_json": "JSON 格式错误", - "login": "登录 Github", - "logout": "退出 Github", - "logout_failed": "退出失败,请重试", - "logout_success": "已成功退出", - "model_setting": "模型设置", - "open_verification_first": "请先点击上方链接访问验证页面", - "rate_limit": "速率限制", - "tooltip": "使用 Github Copilot 需要先登录 Github" - }, - "delete.content": "确定要删除此模型提供商吗?", - "delete.title": "删除提供商", - "docs_check": "查看", - "docs_more_details": "获取更多详情", - "get_api_key": "点击这里获取密钥", - "is_not_support_array_content": "开启兼容模式", - "no_models_for_check": "没有可以被检查的模型(例如对话模型)", - "not_checked": "未检查", - "remove_duplicate_keys": "移除重复密钥", - "remove_invalid_keys": "删除无效密钥", - "search": "搜索模型平台...", - "search_placeholder": "搜索模型 ID 或名称", - "title": "模型服务" - }, - "proxy": { - "mode": { - "custom": "自定义代理", - "none": "不使用代理", - "system": "系统代理", - "title": "代理模式" - }, - "title": "代理设置" - }, - "proxy.title": "代理地址", - "quickAssistant": { - "click_tray_to_show": "点击托盘图标启动", - "enable_quick_assistant": "启用快捷助手", - "read_clipboard_at_startup": "启动时读取剪贴板", - "title": "快捷助手", - "use_shortcut_to_show": "右键点击托盘图标或使用快捷键启动" - }, - "shortcuts": { - "action": "操作", - "clear_shortcut": "清除快捷键", - "clear_topic": "清空消息", - "copy_last_message": "复制上一条消息", - "key": "按键", - "mini_window": "快捷助手", - "new_topic": "新建话题", - "press_shortcut": "按下快捷键", - "reset_defaults": "重置默认快捷键", - "reset_defaults_confirm": "确定要重置所有快捷键吗?", - "reset_to_default": "重置为默认", - "search_message": "搜索消息", - "show_app": "显示/隐藏应用", - "show_settings": "打开设置", - "title": "快捷方式", - "toggle_new_context": "清除上下文", - "toggle_show_assistants": "切换助手显示", - "toggle_show_topics": "切换话题显示", - "zoom_in": "放大界面", - "zoom_out": "缩小界面", - "zoom_reset": "重置缩放" - }, - "theme.auto": "自动", - "theme.dark": "深色", - "theme.light": "浅色", - "theme.title": "主题", - "theme.window.style.opaque": "不透明窗口", - "theme.window.style.title": "窗口样式", - "theme.window.style.transparent": "透明窗口", - "title": "设置", - "topic.position": "话题位置", - "topic.position.left": "左侧", - "topic.position.right": "右侧", - "topic.show.time": "显示话题时间", - "tray.onclose": "关闭时最小化到托盘", - "tray.show": "显示托盘图标", - "tray.title": "托盘", - "websearch": { - "blacklist": "黑名单", - "blacklist_description": "在搜索结果中不会出现以下网站的结果", - "blacklist_tooltip": "请使用以下格式(换行分隔)\n匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/", - "check": "检查", - "check_failed": "验证失败", - "check_success": "验证成功", - "enhance_mode": "搜索增强模式", - "enhance_mode_tooltip": "使用默认模型提取关键词后搜索", - "overwrite": "覆盖服务商搜索", - "overwrite_tooltip": "强制使用搜索服务商而不是大语言模型进行搜索", - "get_api_key": "点击这里获取密钥", - "no_provider_selected": "请选择搜索服务商后再检查", - "search_max_result": "搜索结果个数", - "search_provider": "搜索服务商", - "search_provider_placeholder": "选择一个搜索服务商", - "subscribe": "黑名单订阅", - "subscribe_update": "立即更新", - "subscribe_add": "添加订阅", - "subscribe_url": "订阅源地址", - "subscribe_name": "替代名字", - "subscribe_name.placeholder": "当下载的订阅源没有名称时所使用的替代名称", - "subscribe_add_success": "订阅源添加成功!", - "subscribe_delete": "删除订阅源", - "search_result_default": "默认", - "search_with_time": "搜索包含日期", - "tavily": { - "api_key": "Tavily API 密钥", - "api_key.placeholder": "请输入 Tavily API 密钥", - "description": "Tavily 是一个为 AI 代理量身定制的搜索引擎,提供实时、准确的结果、智能查询建议和深入的研究能力", - "title": "Tavily" - }, - "title": "网络搜索", - "apikey": "API 密钥", - "free": "免费" - }, - "quickPhrase": { - "title": "快捷短语", - "add": "添加短语", - "edit": "编辑短语", - "titleLabel": "标题", - "contentLabel": "内容", - "titlePlaceholder": "请输入短语标题", - "contentPlaceholder": "请输入短语内容,支持使用变量,然后按Tab键可以快速定位到变量进行修改。比如:\n帮我规划从${from}到${to}的路线,然后发送到${email}。", - "delete": "删除短语", - "deleteConfirm": "删除短语后将无法恢复,是否继续?" - }, - "quickPanel": { - "title": "快捷菜单", - "close": "关闭", - "select": "选择", - "page": "翻页", - "confirm": "确认", - "back": "后退", - "forward": "前进", - "multiple": "多选" - }, - "privacy": { - "title": "隐私设置", - "enable_privacy_mode": "匿名发送错误报告和数据统计" - }, - "voice": { - "title": "语音功能", - "help": "语音功能包括文本转语音(TTS)、语音识别(ASR)和语音通话。", - "learn_more": "了解更多" - }, - "tts": { - "title": "语音合成", - "tab_title": "语音合成", - "enable": "启用语音合成", - "enable.help": "启用后可以将文本转换为语音", - "reset": "重置", - "reset_title": "重置自定义音色和模型", - "reset_confirm": "确定要重置所有自定义音色和模型吗?这将删除所有已添加的自定义项。", - "reset_success": "重置成功", - "reset_help": "如果音色或模型显示异常,可以尝试重置所有自定义项", - "api_settings": "API设置", - "service_type": "服务类型", - "service_type.openai": "OpenAI", - "service_type.edge": "浏览器 TTS", - "service_type.siliconflow": "硅基流动", - "service_type.mstts": "免费在线 TTS", - "service_type.refresh": "刷新TTS服务类型设置", - "service_type.refreshed": "已刷新TTS服务类型设置", - "siliconflow_api_key": "硅基流动API密钥", - "siliconflow_api_key.placeholder": "请输入硅基流动API密钥", - "siliconflow_api_url": "硅基流动API地址", - "siliconflow_api_url.placeholder": "例如:https://api.siliconflow.cn/v1/audio/speech", - "siliconflow_voice": "硅基流动音色", - "siliconflow_voice.placeholder": "请选择音色", - "siliconflow_model": "硅基流动模型", - "siliconflow_model.placeholder": "请选择模型", - "siliconflow_response_format": "响应格式", - "siliconflow_response_format.placeholder": "默认为mp3", - "siliconflow_speed": "语速", - "siliconflow_speed.placeholder": "默认为1.0", - "api_key": "API密钥", - "api_key.placeholder": "请输入OpenAI API密钥", - "api_url": "API地址", - "api_url.placeholder": "例如:https://api.openai.com/v1/audio/speech", - "edge_voice": "浏览器 TTS音色", - "edge_voice.loading": "加载中...", - "edge_voice.refresh": "刷新可用音色列表", - "edge_voice.not_found": "未找到匹配的音色", - "edge_voice.available_count": "可用语音: {{count}}个", - "edge_voice.refreshing": "正在刷新语音列表...", - "edge_voice.refreshed": "语音列表已刷新", - "mstts.voice": "免费在线 TTS音色", - "mstts.output_format": "输出格式", - "mstts.info": "免费在线TTS服务不需要API密钥,完全免费使用。", - "error.no_mstts_voice": "未设置免费在线 TTS音色", - "voice": "音色", - "voice.placeholder": "请选择音色", - "voice_input_placeholder": "输入音色", - "voice_add": "添加", - "voice_empty": "暂无自定义音色,请在下方添加", - "model": "模型", - "model.placeholder": "请选择模型", - "model_input_placeholder": "输入模型", - "model_add": "添加", - "model_empty": "暂无自定义模型,请在下方添加", - "filter_options": "过滤选项", - "filter.thinking_process": "过滤思考过程", - "filter.markdown": "过滤Markdown标记", - "filter.code_blocks": "过滤代码块", - "filter.html_tags": "过滤HTML标签", - "filter.emojis": "过滤表情符号", - "max_text_length": "最大文本长度", - "show_progress_bar": "显示TTS进度条", - "test": "测试语音", - "help": "语音合成功能支持将文本转换为自然语音。", - "learn_more": "了解更多", - "play": "播放语音", - "stop": "停止播放", - "speak": "播放语音", - "stop_global": "停止所有语音播放", - "stopped": "已停止语音播放", - "segmented": "分段", - "segmented_play": "分段播放", - "segmented_playback": "分段播放", - "error": { - "not_enabled": "语音合成功能未启用", - "no_api_key": "未设置API密钥", - "no_voice": "未选择音色", - "no_model": "未选择模型", - "no_edge_voice": "未选择浏览器 TTS音色", - "browser_not_support": "浏览器不支持语音合成", - "synthesis_failed": "语音合成失败", - "play_failed": "语音播放失败", - "empty_text": "文本为空", - "general": "语音合成出现错误", - "unsupported_service_type": "不支持的服务类型: {{serviceType}}" - } - }, - "asr": { - "title": "语音识别", - "tab_title": "语音识别", - "enable": "启用语音识别", - "enable.help": "启用后可以将语音转换为文本", - "service_type": "服务类型", - "service_type.browser": "浏览器", - "service_type.local": "本地服务器", - "api_key": "API密钥", - "api_key.placeholder": "请输入OpenAI API密钥", - "api_url": "API地址", - "api_url.placeholder": "例如:https://api.openai.com/v1/audio/transcriptions", - "model": "模型", - "browser.info": "使用浏览器内置的语音识别功能,无需额外设置", - "local.info": "使用本地服务器和浏览器进行语音识别,需要先启动服务器并打开浏览器页面", - "local.browser_tip": "请在浏览器中打开此页面,并保持浏览器窗口打开", - "local.test_connection": "测试连接", - "local.connection_success": "连接成功", - "local.connection_failed": "连接失败,请确保服务器已启动", - "server.start": "启动服务器", - "server.stop": "停止服务器", - "server.starting": "正在启动服务器...", - "server.started": "服务器已启动", - "server.stopping": "正在停止服务器...", - "server.stopped": "服务器已停止", - "server.already_running": "服务器已经在运行中", - "server.not_running": "服务器未运行", - "server.start_failed": "启动服务器失败", - "server.stop_failed": "停止服务器失败", - "open_browser": "打开浏览器页面", - "test": "测试语音识别", - "test_info": "请在输入框中使用语音识别按钮进行测试", - "start": "开始录音", - "stop": "停止录音", - "preparing": "准备中", - "recording": "正在录音...", - "processing": "正在处理语音...", - "success": "语音识别成功", - "completed": "语音识别完成", - "canceled": "已取消录音", - "auto_start_server": "启动应用自动开启服务器", - "auto_start_server.help": "启用后,应用启动时会自动开启语音识别服务器", - "error": { - "not_enabled": "语音识别功能未启用", - "no_api_key": "未设置API密钥", - "browser_not_support": "浏览器不支持语音识别", - "start_failed": "开始录音失败", - "transcribe_failed": "语音识别失败" - } - }, - "voice_call": { - "tab_title": "通话功能", - "enable": "启用语音通话", - "enable.help": "启用后可以使用语音通话功能与AI进行对话", - "model": "通话模型", - "model.select": "选择模型", - "model.current": "当前模型: {{model}}", - "model.info": "选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", - "welcome_message": "您好,我是您的AI助手,请长按说话按钮进行对话。", - "prompt": { - "label": "语音通话提示词", - "placeholder": "请输入语音通话提示词", - "save": "保存", - "reset": "重置", - "saved": "提示词已保存", - "reset_done": "提示词已重置", - "info": "此提示词将指导AI在语音通话模式下的回复方式" - }, - "asr_tts_info": "语音通话使用上面的语音识别(ASR)和语音合成(TTS)设置", - "test": "测试通话", - "test_info": "请使用输入框右侧的语音通话按钮进行测试" - } - }, - "translate": { - "any.language": "任意语言", - "button.translate": "翻译", - "close": "关闭", - "confirm": { - "content": "翻译后将覆盖原文,是否继续?", - "title": "翻译确认" - }, - "error.failed": "翻译失败", - "error.not_configured": "翻译模型未配置", - "history": { - "clear": "清空历史", - "clear_description": "清空历史将删除所有翻译历史记录,是否继续?", - "delete": "删除", - "empty": "暂无翻译历史", - "title": "翻译历史" - }, - "menu": { - "description": "对当前输入框内容进行翻译" - }, - "input.placeholder": "输入文本进行翻译", - "output.placeholder": "翻译", - "processing": "翻译中...", - "scroll_sync.disable": "关闭滚动同步", - "scroll_sync.enable": "开启滚动同步", - "title": "翻译", - "tooltip.newline": "换行" - }, - "tray": { - "quit": "退出", - "show_mini_window": "快捷助手", - "show_window": "显示窗口" - }, - "words": { - "knowledgeGraph": "知识图谱", - "quit": "退出", - "show_window": "显示窗口", - "visualization": "可视化" - } - } -} From a8e9affb8624aacafbcdef13a83608b6fc9c049d Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Sun, 13 Apr 2025 21:50:00 +0800 Subject: [PATCH 53/59] chore: update yarn.lock and enhance localization in Japanese, Russian, and Traditional Chinese - Removed unused dependencies from yarn.lock. - Added new localization strings for emoji filtering and TTS progress bar in Japanese, Russian, and Traditional Chinese. - Improved layout and styling in TTSSettings and VoiceCallSettings components. --- src/renderer/src/i18n/locales/ja-jp.json | 10 +- src/renderer/src/i18n/locales/ru-ru.json | 10 +- src/renderer/src/i18n/locales/zh-tw.json | 10 +- .../settings/TTSSettings/TTSSettings.tsx | 6 +- .../TTSSettings/VoiceCallSettings.tsx | 93 ++++++++----------- yarn.lock | 36 +------ 6 files changed, 62 insertions(+), 103 deletions(-) diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 7d3f5229b6..fd89b97065 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1458,7 +1458,9 @@ "stopped": "音声再生を停止しました", "segmented": "分割", "segmented_play": "分割再生", - "segmented_playback": "分割再生" + "segmented_playback": "分割再生", + "filter.emojis": "[to be translated]:过滤表情符号", + "show_progress_bar": "[to be translated]:显示TTS进度条" }, "asr": { "title": "音声認識", @@ -1508,8 +1510,7 @@ "browser_not_support": "ブラウザが音声認識をサポートしていません" }, "auto_start_server": "アプリ起動時にサーバーを自動起動", - "auto_start_server.help": "有効にすると、アプリ起動時に音声認識サーバーが自動的に起動します", - "language": "認識言語" + "auto_start_server.help": "有効にすると、アプリ起動時に音声認識サーバーが自動的に起動します" }, "voice": { "title": "音声機能", @@ -1532,8 +1533,7 @@ "reset": "リセット", "saved": "プロンプトが保存されました", "reset_done": "プロンプトがリセットされました", - "info": "このプロンプトは音声通話モードでのAIの応答方法を指導します", - "language_info": "リセットボタンをクリックすると、現在の言語のデフォルトプロンプトが取得されます" + "info": "このプロンプトは音声通話モードでのAIの応答方法を指導します" }, "asr_tts_info": "音声通話は上記の音声認識(ASR)と音声合成(TTS)の設定を使用します", "test": "音声通話テスト", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index c518d8aa73..1780e307c7 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1458,7 +1458,9 @@ "stopped": "[to be translated]:已停止语音播放", "segmented": "[to be translated]:分段", "segmented_play": "[to be translated]:分段播放", - "segmented_playback": "[to be translated]:分段播放" + "segmented_playback": "[to be translated]:分段播放", + "filter.emojis": "[to be translated]:过滤表情符号", + "show_progress_bar": "[to be translated]:显示TTS进度条" }, "voice": { "title": "[to be translated]:语音功能", @@ -1530,12 +1532,12 @@ "reset": "Сбросить", "saved": "Подсказка сохранена", "reset_done": "Подсказка сброшена", - "info": "Эта подсказка будет направлять ответы ИИ в режиме голосового вызова", - "language_info": "Нажмите кнопку сброса, чтобы получить стандартную подсказку для текущего языка" + "info": "Эта подсказка будет направлять ответы ИИ в режиме голосового вызова" }, "asr_tts_info": "Голосовой вызов использует настройки распознавания речи (ASR) и синтеза речи (TTS), указанные выше", "test": "Тестировать голосовой вызов", - "test_info": "Используйте кнопку голосового вызова справа от поля ввода для тестирования" + "test_info": "Используйте кнопку голосового вызова справа от поля ввода для тестирования", + "welcome_message": "[to be translated]:您好,我是您的AI助手,请长按说话按钮进行对话。" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 339f938cf6..81d4a1355d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1458,7 +1458,9 @@ "stopped": "[to be translated]:已停止语音播放", "segmented": "[to be translated]:分段", "segmented_play": "[to be translated]:分段播放", - "segmented_playback": "[to be translated]:分段播放" + "segmented_playback": "[to be translated]:分段播放", + "filter.emojis": "[to be translated]:过滤表情符号", + "show_progress_bar": "[to be translated]:显示TTS进度条" }, "voice": { "title": "[to be translated]:语音功能", @@ -1530,12 +1532,12 @@ "reset": "重置", "saved": "提示詞已保存", "reset_done": "提示詞已重置", - "info": "此提示詞將指導AI在語音通話模式下的回覆方式", - "language_info": "點擊重置按鈕可獲取當前語言的預設提示詞" + "info": "此提示詞將指導AI在語音通話模式下的回覆方式" }, "asr_tts_info": "語音通話使用上面的語音識別(ASR)和語音合成(TTS)設置", "test": "測試通話", - "test_info": "請使用輸入框右側的語音通話按鈕進行測試" + "test_info": "請使用輸入框右側的語音通話按鈕進行測試", + "welcome_message": "[to be translated]:您好,我是您的AI助手,请长按说话按钮进行对话。" } }, "translate": { diff --git a/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx index dcddbf0e94..a45f8ed69c 100644 --- a/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx +++ b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx @@ -1043,7 +1043,11 @@ const TTSSettings: FC = () => { /> {t('settings.voice.help')} - + {t('settings.voice.learn_more')} diff --git a/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx index dd24729eda..0b647824ac 100644 --- a/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx +++ b/src/renderer/src/pages/settings/TTSSettings/VoiceCallSettings.tsx @@ -66,7 +66,6 @@ const VoiceCallSettings: FC = () => { - {/* 模型选择 */} @@ -75,7 +74,11 @@ const VoiceCallSettings: FC = () => { disabled={!voiceCallEnabled} icon={ voiceCallModel ? ( - + ) : ( ) @@ -88,7 +91,6 @@ const VoiceCallSettings: FC = () => { {t('settings.voice_call.model.info')} - {/* 提示词设置 */} { rows={8} placeholder={t('settings.voice_call.prompt.placeholder')} /> + {t('settings.voice_call.prompt.info')} - {t('settings.voice_call.prompt.info')} - - - {/* ASR 和 TTS 设置提示 */} - - {t('settings.voice_call.asr_tts_info')} - - - {/* 测试按钮 */} - -
@@ -142,39 +126,40 @@ const InfoText = styled.div` ` const ModelIcon = styled.img` - width: 16px; - height: 16px; - margin-right: 8px; + width: 20px; + height: 20px; + border-radius: 10px; + margin-top: 4px; ` -const Alert = styled.div<{ type: 'info' | 'warning' | 'error' | 'success' }>` - padding: 8px 12px; - border-radius: 4px; - background-color: ${(props) => - 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)'}; - 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)'}; - 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)'}; -` +// const Alert = styled.div<{ type: 'info' | 'warning' | 'error' | 'success' }>` +// padding: 8px 12px; +// border-radius: 4px; +// background-color: ${(props) => +// 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)'}; +// 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)'}; +// 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)'}; +// ` export default VoiceCallSettings diff --git a/yarn.lock b/yarn.lock index cceefb5303..4bdda97931 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3946,7 +3946,6 @@ __metadata: axios: "npm:^1.7.3" babel-plugin-styled-components: "npm:^2.1.4" browser-image-compression: "npm:^2.0.2" - bufferutil: "npm:^4.0.9" color: "npm:^5.0.0" dayjs: "npm:^1.11.11" dexie: "npm:^4.0.8" @@ -4024,11 +4023,9 @@ __metadata: turndown-plugin-gfm: "npm:^1.0.2" typescript: "npm:^5.6.2" undici: "npm:^7.4.0" - utf-8-validate: "npm:^6.0.5" uuid: "npm:^10.0.0" vite: "npm:^5.0.12" webdav: "npm:^5.8.0" - ws: "npm:^8.18.1" zipread: "npm:^1.3.3" languageName: unknown linkType: soft @@ -4952,16 +4949,6 @@ __metadata: languageName: node linkType: hard -"bufferutil@npm:^4.0.9": - version: 4.0.9 - resolution: "bufferutil@npm:4.0.9" - dependencies: - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.3.0" - checksum: 10c0/f8a93279fc9bdcf32b42eba97edc672b39ca0fe5c55a8596099886cffc76ea9dd78e0f6f51ecee3b5ee06d2d564aa587036b5d4ea39b8b5ac797262a363cdf7d - languageName: node - linkType: hard - "builder-util-runtime@npm:9.2.4": version: 9.2.4 resolution: "builder-util-runtime@npm:9.2.4" @@ -12140,17 +12127,6 @@ __metadata: languageName: node linkType: hard -"node-gyp-build@npm:^4.3.0": - version: 4.8.4 - resolution: "node-gyp-build@npm:4.8.4" - bin: - node-gyp-build: bin.js - node-gyp-build-optional: optional.js - node-gyp-build-test: build-test.js - checksum: 10c0/444e189907ece2081fe60e75368784f7782cfddb554b60123743dfb89509df89f1f29c03bbfa16b3a3e0be3f48799a4783f487da6203245fa5bed239ba7407e1 - languageName: node - linkType: hard - "node-gyp@npm:^9.1.0": version: 9.4.1 resolution: "node-gyp@npm:9.4.1" @@ -16679,16 +16655,6 @@ __metadata: languageName: node linkType: hard -"utf-8-validate@npm:^6.0.5": - version: 6.0.5 - resolution: "utf-8-validate@npm:6.0.5" - dependencies: - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.3.0" - checksum: 10c0/6dc63c513adb001e47a51819072cdd414158430091c49c21d4947ea99f16df5167b671f680df8fb2b6f2ae6a7f30264b4ec111bd3e573720dfe371da1ab99a81 - languageName: node - linkType: hard - "utf8-byte-length@npm:^1.0.1": version: 1.0.5 resolution: "utf8-byte-length@npm:1.0.5" @@ -17123,7 +17089,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.14.1, ws@npm:^8.18.0, ws@npm:^8.18.1": +"ws@npm:^8.13.0, ws@npm:^8.14.1, ws@npm:^8.18.0": version: 8.18.1 resolution: "ws@npm:8.18.1" peerDependencies: From 8b9d3a2e56d72fb9776a7a9d0663eaebe792e5cd Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Mon, 14 Apr 2025 00:16:56 +0800 Subject: [PATCH 54/59] =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/i18n/locales/en-us.json | 18 +- src/renderer/src/i18n/locales/ja-jp.json | 4 +- src/renderer/src/i18n/locales/ru-ru.json | 291 ++++++++++++----------- src/renderer/src/i18n/locales/zh-tw.json | 288 +++++++++++----------- 4 files changed, 302 insertions(+), 299 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index bd2230a25d..b662bcd4c3 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -16,11 +16,11 @@ "error": "Voice call error", "initializing": "Initializing voice call...", "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]:按下此快捷键开始录音,松开快捷键结束录音并发送" + "shortcut_key_setting": "Voice Recognition Shortcut Key Settings", + "press_any_key": "Press any key...", + "save": "Save", + "cancel": "Cancel", + "shortcut_key_tip": "Press this shortcut key to start recording, release to end recording and send" }, "agents": { "add.button": "Add to Assistant", @@ -1528,11 +1528,11 @@ "not_enabled": "Speech recognition is not enabled", "start_failed": "Failed to start recording", "transcribe_failed": "Failed to transcribe speech", - "no_api_key": "[to be translated]:未设置API密钥", - "browser_not_support": "[to be translated]:浏览器不支持语音识别" + "no_api_key": "API key is not set", + "browser_not_support": "Browser does not support speech recognition" }, - "auto_start_server": "[to be translated]:启动应用自动开启服务器", - "auto_start_server.help": "[to be translated]:启用后,应用启动时会自动开启语音识别服务器" + "auto_start_server": "Automatically start server when launching the application", + "auto_start_server.help": "When enabled, the speech recognition server will automatically start when the application launches" }, "voice": { "title": "Voice Features", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index fd89b97065..7399d787b7 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1459,8 +1459,8 @@ "segmented": "分割", "segmented_play": "分割再生", "segmented_playback": "分割再生", - "filter.emojis": "[to be translated]:过滤表情符号", - "show_progress_bar": "[to be translated]:显示TTS进度条" + "filter.emojis": "絵文字をフィルター", + "show_progress_bar": "TTS進行バーを表示" }, "asr": { "title": "音声認識", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 1780e307c7..d9bca174d6 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -331,6 +331,9 @@ "503": "Серверная ошибка. Пожалуйста, попробуйте позже", "504": "Серверная ошибка. Пожалуйста, попробуйте позже" }, + "asr": { + "browser_not_support": "Браузер не поддерживает распознавание речи" + }, "model.exists": "Модель уже существует", "no_api_key": "Ключ API не настроен", "provider_disabled": "Провайдер моделей не включен", @@ -1391,140 +1394,140 @@ "error": { "not_enabled": "Преобразование текста в речь не включено", "no_edge_voice": "Голос Edge TTS не выбран", - "no_api_key": "[to be translated]:未设置API密钥", - "browser_not_support": "[to be translated]:浏览器不支持语音合成", - "no_voice": "[to be translated]:未选择音色", - "no_model": "[to be translated]:未选择模型", - "synthesis_failed": "[to be translated]:语音合成失败", - "play_failed": "[to be translated]:语音播放失败", - "empty_text": "[to be translated]:文本为空", - "general": "[to be translated]:语音合成出现错误", - "unsupported_service_type": "[to be translated]:不支持的服务类型: {{serviceType}}" + "no_api_key": "Ключ API не настроен", + "browser_not_support": "Браузер не поддерживает синтез речи", + "no_voice": "Голос не выбран", + "no_model": "Модель не выбрана", + "synthesis_failed": "Ошибка синтеза речи", + "play_failed": "Ошибка воспроизведения речи", + "empty_text": "Текст пуст", + "general": "Ошибка синтеза речи", + "unsupported_service_type": "Неподдерживаемый тип службы: {{serviceType}}" }, "help": "Для использования API TTS OpenAI требуется ключ API. Edge TTS использует функции браузера и не требует ключа API.", "learn_more": "Узнать больше", - "tab_title": "[to be translated]:语音合成", - "service_type.refresh": "[to be translated]:刷新TTS服务类型设置", - "service_type.refreshed": "[to be translated]:已刷新TTS服务类型设置", - "api_key": "[to be translated]:API密钥", - "api_key.placeholder": "[to be translated]:请输入OpenAI API密钥", - "api_url": "[to be translated]:API地址", - "api_url.placeholder": "[to be translated]:例如:https://api.openai.com/v1/audio/speech", - "edge_voice": "[to be translated]:浏览器 TTS音色", - "edge_voice.loading": "[to be translated]:加载中...", - "edge_voice.refresh": "[to be translated]:刷新可用音色列表", - "edge_voice.not_found": "[to be translated]:未找到匹配的音色", - "voice": "[to be translated]:音色", - "voice.placeholder": "[to be translated]:请选择音色", - "voice_input_placeholder": "[to be translated]:输入音色", - "voice_add": "[to be translated]:添加", - "voice_empty": "[to be translated]:暂无自定义音色,请在下方添加", - "model": "[to be translated]:模型", - "model.placeholder": "[to be translated]:请选择模型", - "model_input_placeholder": "[to be translated]:输入模型", - "model_add": "[to be translated]:添加", - "model_empty": "[to be translated]:暂无自定义模型,请在下方添加", - "filter_options": "[to be translated]:过滤选项", - "filter.thinking_process": "[to be translated]:过滤思考过程", - "filter.markdown": "[to be translated]:过滤Markdown标记", - "filter.code_blocks": "[to be translated]:过滤代码块", - "filter.html_tags": "[to be translated]:过滤HTML标签", - "max_text_length": "[to be translated]:最大文本长度", - "service_type.siliconflow": "[to be translated]:硅基流动", - "service_type.mstts": "[to be translated]:免费在线 TTS", - "siliconflow_api_key": "[to be translated]:硅基流动API密钥", - "siliconflow_api_key.placeholder": "[to be translated]:请输入硅基流动API密钥", - "siliconflow_api_url": "[to be translated]:硅基流动API地址", - "siliconflow_api_url.placeholder": "[to be translated]:例如:https://api.siliconflow.cn/v1/audio/speech", - "siliconflow_voice": "[to be translated]:硅基流动音色", - "siliconflow_voice.placeholder": "[to be translated]:请选择音色", - "siliconflow_model": "[to be translated]:硅基流动模型", - "siliconflow_model.placeholder": "[to be translated]:请选择模型", - "siliconflow_response_format": "[to be translated]:响应格式", - "siliconflow_response_format.placeholder": "[to be translated]:默认为mp3", - "siliconflow_speed": "[to be translated]:语速", - "siliconflow_speed.placeholder": "[to be translated]:默认为1.0", - "edge_voice.available_count": "[to be translated]:可用语音: {{count}}个", - "edge_voice.refreshing": "[to be translated]:正在刷新语音列表...", - "edge_voice.refreshed": "[to be translated]:语音列表已刷新", - "mstts.voice": "[to be translated]:免费在线 TTS音色", - "mstts.output_format": "[to be translated]:输出格式", - "mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。", - "error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色", - "play": "[to be translated]:播放语音", - "stop": "[to be translated]:停止播放", - "speak": "[to be translated]:播放语音", - "stop_global": "[to be translated]:停止所有语音播放", - "stopped": "[to be translated]:已停止语音播放", - "segmented": "[to be translated]:分段", - "segmented_play": "[to be translated]:分段播放", - "segmented_playback": "[to be translated]:分段播放", - "filter.emojis": "[to be translated]:过滤表情符号", - "show_progress_bar": "[to be translated]:显示TTS进度条" + "tab_title": "Синтез речи", + "service_type.refresh": "Обновить настройки типа службы TTS", + "service_type.refreshed": "Настройки типа службы TTS обновлены", + "api_key": "Ключ API", + "api_key.placeholder": "Пожалуйста, введите ключ API OpenAI", + "api_url": "URL-адрес API", + "api_url.placeholder": "Например: https://api.openai.com/v1/audio/speech", + "edge_voice": "Голос TTS браузера", + "edge_voice.loading": "Загрузка...", + "edge_voice.refresh": "Обновить список доступных голосов", + "edge_voice.not_found": "Не найдено подходящего голоса", + "voice": "Голос", + "voice.placeholder": "Пожалуйста, выберите голос", + "voice_input_placeholder": "Введите голос", + "voice_add": "Добавить", + "voice_empty": "Пользовательские голоса отсутствуют, пожалуйста, добавьте ниже", + "model": "Модель", + "model.placeholder": "Пожалуйста, выберите модель", + "model_input_placeholder": "Введите модель", + "model_add": "Добавить", + "model_empty": "Пользовательские модели отсутствуют, пожалуйста, добавьте ниже", + "filter_options": "Параметры фильтрации", + "filter.thinking_process": "Фильтровать процесс рассуждения", + "filter.markdown": "Фильтровать разметку Markdown", + "filter.code_blocks": "Фильтровать блоки кода", + "filter.html_tags": "Фильтровать HTML-теги", + "max_text_length": "Максимальная длина текста", + "service_type.siliconflow": "SiliconFlow", + "service_type.mstts": "Бесплатный онлайн TTS", + "siliconflow_api_key": "Ключ API SiliconFlow", + "siliconflow_api_key.placeholder": "Пожалуйста, введите ключ API SiliconFlow", + "siliconflow_api_url": "URL-адрес API SiliconFlow", + "siliconflow_api_url.placeholder": "Например: https://api.siliconflow.cn/v1/audio/speech", + "siliconflow_voice": "Голос SiliconFlow", + "siliconflow_voice.placeholder": "Пожалуйста, выберите голос", + "siliconflow_model": "Модель SiliconFlow", + "siliconflow_model.placeholder": "Пожалуйста, выберите модель", + "siliconflow_response_format": "Формат ответа", + "siliconflow_response_format.placeholder": "По умолчанию mp3", + "siliconflow_speed": "Скорость речи", + "siliconflow_speed.placeholder": "По умолчанию 1.0", + "edge_voice.available_count": "Доступные голоса: {{count}}", + "edge_voice.refreshing": "Обновление списка голосов...", + "edge_voice.refreshed": "Список голосов обновлен", + "mstts.voice": "Бесплатный онлайн голос TTS", + "mstts.output_format": "Формат вывода", + "mstts.info": "Бесплатная онлайн-служба TTS не требует ключа API и полностью бесплатна для использования.", + "error.no_mstts_voice": "Бесплатный онлайн голос TTS не установлен", + "play": "Воспроизвести речь", + "stop": "Остановить воспроизведение", + "speak": "Воспроизвести речь", + "stop_global": "Остановить все воспроизведения речи", + "stopped": "Воспроизведение речи остановлено", + "segmented": "Сегментация", + "segmented_play": "Сегментированное воспроизведение", + "segmented_playback": "Сегментированное воспроизведение", + "filter.emojis": "Фильтровать эмодзи", + "show_progress_bar": "Показать индикатор выполнения TTS" }, "voice": { - "title": "[to be translated]:语音功能", - "help": "[to be translated]:语音功能包括文本转语音(TTS)和语音识别(ASR)。", - "learn_more": "[to be translated]:了解更多" + "title": "Голосовые функции", + "help": "Голосовые функции включают преобразование текста в речь (TTS) и распознавание речи (ASR).", + "learn_more": "Узнать больше" }, "asr": { - "title": "[to be translated]:语音识别", - "tab_title": "[to be translated]:语音识别", - "enable": "[to be translated]:启用语音识别", - "enable.help": "[to be translated]:启用后可以将语音转换为文本", - "service_type": "[to be translated]:服务类型", - "service_type.browser": "[to be translated]:浏览器", - "service_type.local": "[to be translated]:本地服务器", - "api_key": "[to be translated]:API密钥", - "api_key.placeholder": "[to be translated]:请输入OpenAI API密钥", - "api_url": "[to be translated]:API地址", - "api_url.placeholder": "[to be translated]:例如:https://api.openai.com/v1/audio/transcriptions", - "model": "[to be translated]:模型", - "browser.info": "[to be translated]:使用浏览器内置的语音识别功能,无需额外设置", - "local.info": "[to be translated]:使用本地服务器和浏览器进行语音识别,需要先启动服务器并打开浏览器页面", - "local.browser_tip": "[to be translated]:请在浏览器中打开此页面,并保持浏览器窗口打开", - "local.test_connection": "[to be translated]:测试连接", - "local.connection_success": "[to be translated]:连接成功", - "local.connection_failed": "[to be translated]:连接失败,请确保服务器已启动", - "server.start": "[to be translated]:启动服务器", - "server.stop": "[to be translated]:停止服务器", - "server.starting": "[to be translated]:正在启动服务器...", - "server.started": "[to be translated]:服务器已启动", - "server.stopping": "[to be translated]:正在停止服务器...", - "server.stopped": "[to be translated]:服务器已停止", - "server.already_running": "[to be translated]:服务器已经在运行中", - "server.not_running": "[to be translated]:服务器未运行", - "server.start_failed": "[to be translated]:启动服务器失败", - "server.stop_failed": "[to be translated]:停止服务器失败", - "open_browser": "[to be translated]:打开浏览器页面", - "test": "[to be translated]:测试语音识别", - "test_info": "[to be translated]:请在输入框中使用语音识别按钮进行测试", - "start": "[to be translated]:开始录音", - "stop": "[to be translated]:停止录音", - "preparing": "[to be translated]:准备中", - "recording": "[to be translated]:正在录音...", - "processing": "[to be translated]:正在处理语音...", - "success": "[to be translated]:语音识别成功", - "completed": "[to be translated]:语音识别完成", - "canceled": "[to be translated]:已取消录音", + "title": "Распознавание речи", + "tab_title": "Распознавание речи", + "enable": "Включить распознавание речи", + "enable.help": "После включения можно преобразовывать речь в текст", + "service_type": "Тип службы", + "service_type.browser": "Браузер", + "service_type.local": "Локальный сервер", + "api_key": "Ключ API", + "api_key.placeholder": "Пожалуйста, введите ключ API OpenAI", + "api_url": "URL-адрес API", + "api_url.placeholder": "Например: https://api.openai.com/v1/audio/transcriptions", + "model": "Модель", + "browser.info": "Используйте встроенную функцию распознавания речи браузера, дополнительные настройки не требуются", + "local.info": "Используйте локальный сервер и браузер для распознавания речи, необходимо сначала запустить сервер и открыть страницу браузера", + "local.browser_tip": "Пожалуйста, откройте эту страницу в браузере и держите окно браузера открытым", + "local.test_connection": "Тестировать соединение", + "local.connection_success": "Соединение успешно", + "local.connection_failed": "Соединение не удалось, убедитесь, что сервер запущен", + "server.start": "Запустить сервер", + "server.stop": "Остановить сервер", + "server.starting": "Запуск сервера...", + "server.started": "Сервер запущен", + "server.stopping": "Остановка сервера...", + "server.stopped": "Сервер остановлен", + "server.already_running": "Сервер уже запущен", + "server.not_running": "Сервер не запущен", + "server.start_failed": "Не удалось запустить сервер", + "server.stop_failed": "Не удалось остановить сервер", + "open_browser": "Открыть страницу в браузере", + "test": "Тестировать распознавание речи", + "test_info": "Используйте кнопку распознавания речи в поле ввода для тестирования", + "start": "Начать запись", + "stop": "Остановить запись", + "preparing": "Подготовка", + "recording": "Запись...", + "processing": "Обработка речи...", + "success": "Распознавание речи успешно", + "completed": "Распознавание речи завершено", + "canceled": "Запись отменена", "error": { - "not_enabled": "[to be translated]:语音识别功能未启用", - "no_api_key": "[to be translated]:未设置API密钥", - "browser_not_support": "[to be translated]:浏览器不支持语音识别", - "start_failed": "[to be translated]:开始录音失败", - "transcribe_failed": "[to be translated]:语音识别失败" + "not_enabled": "Функция распознавания речи не включена", + "no_api_key": "API ключ не настроен", + "browser_not_support": "Браузер не поддерживает распознавание речи", + "start_failed": "Не удалось начать запись", + "transcribe_failed": "Не удалось распознать речь" }, - "auto_start_server": "[to be translated]:启动应用自动开启服务器", - "auto_start_server.help": "[to be translated]:启用后,应用启动时会自动开启语音识别服务器" + "auto_start_server": "Автоматически запускать сервер при запуске приложения", + "auto_start_server.help": "После включения сервер распознавания речи будет автоматически запускаться при запуске приложения" }, "voice_call": { - "tab_title": "[to be translated]:通话功能", - "enable": "[to be translated]:启用语音通话", - "enable.help": "[to be translated]:启用后可以使用语音通话功能与AI进行对话", - "model": "[to be translated]:通话模型", - "model.select": "[to be translated]:选择模型", - "model.current": "[to be translated]:当前模型: {{model}}", - "model.info": "[to be translated]:选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", + "tab_title": "Функция вызова", + "enable": "Включить голосовой вызов", + "enable.help": "После включения вы сможете использовать функцию голосового вызова для разговора с ИИ", + "model": "Модель вызова", + "model.select": "Выбрать модель", + "model.current": "Текущая модель: {{model}}", + "model.info": "Выберите модель ИИ для голосовых вызовов. Разные модели могут обеспечивать различный опыт голосового взаимодействия", "prompt": { "label": "Подсказка для голосового вызова", "placeholder": "Введите подсказку для голосового вызова", @@ -1537,7 +1540,7 @@ "asr_tts_info": "Голосовой вызов использует настройки распознавания речи (ASR) и синтеза речи (TTS), указанные выше", "test": "Тестировать голосовой вызов", "test_info": "Используйте кнопку голосового вызова справа от поля ввода для тестирования", - "welcome_message": "[to be translated]:您好,我是您的AI助手,请长按说话按钮进行对话。" + "welcome_message": "Здравствуйте, я ваш ИИ-ассистент. Пожалуйста, нажмите и удерживайте кнопку разговора для начала диалога." } }, "translate": { @@ -1580,26 +1583,26 @@ "visualization": "Визуализация" }, "voice_call": { - "title": "[to be translated]:语音通话", - "start": "[to be translated]:开始语音通话", - "end": "[to be translated]:结束通话", - "mute": "[to be translated]:静音", - "unmute": "[to be translated]:取消静音", - "pause": "[to be translated]:暂停", - "resume": "[to be translated]:继续", - "you": "[to be translated]:您", - "ai": "[to be translated]:AI", - "press_to_talk": "[to be translated]:长按说话", - "release_to_send": "[to be translated]:松开发送", - "initialization_failed": "[to be translated]:初始化语音通话失败", - "error": "[to be translated]:语音通话出错", - "initializing": "[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]:按下此快捷键开始录音,松开快捷键结束录音并发送" + "title": "Голосовой вызов", + "start": "Начать голосовой вызов", + "end": "Завершить вызов", + "mute": "Отключить звук", + "unmute": "Включить звук", + "pause": "Пауза", + "resume": "Продолжить", + "you": "Вы", + "ai": "ИИ", + "press_to_talk": "Нажмите и удерживайте для разговора", + "release_to_send": "Отпустите для отправки", + "initialization_failed": "Не удалось инициализировать голосовой вызов", + "error": "Ошибка голосового вызова", + "initializing": "Инициализация голосового вызова...", + "ready": "Голосовой вызов готов", + "shortcut_key_setting": "Настройки горячих клавиш для распознавания речи", + "press_any_key": "Нажмите любую клавишу...", + "save": "Сохранить", + "cancel": "Отмена", + "shortcut_key_tip": "Нажмите эту горячую клавишу, чтобы начать запись, отпустите, чтобы закончить запись и отправить" } } } \ No newline at end of file diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 81d4a1355d..60587db8a8 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1391,140 +1391,140 @@ "error": { "not_enabled": "語音合成未啟用", "no_edge_voice": "未選擇Edge TTS音色", - "no_api_key": "[to be translated]:未设置API密钥", - "browser_not_support": "[to be translated]:浏览器不支持语音合成", - "no_voice": "[to be translated]:未选择音色", - "no_model": "[to be translated]:未选择模型", - "synthesis_failed": "[to be translated]:语音合成失败", - "play_failed": "[to be translated]:语音播放失败", - "empty_text": "[to be translated]:文本为空", - "general": "[to be translated]:语音合成出现错误", - "unsupported_service_type": "[to be translated]:不支持的服务类型: {{serviceType}}" + "no_api_key": "未設定API金鑰", + "browser_not_support": "瀏覽器不支援語音合成", + "no_voice": "未選擇音色", + "no_model": "未選擇模型", + "synthesis_failed": "語音合成失敗", + "play_failed": "語音播放失敗", + "empty_text": "文本為空", + "general": "語音合成出現錯誤", + "unsupported_service_type": "不支援的服務類型: {{serviceType}}" }, "help": "使用OpenAI的TTS API需要API金鑰。Edge TTS使用瀏覽器功能,不需要API金鑰。", "learn_more": "了解更多", - "tab_title": "[to be translated]:语音合成", - "service_type.refresh": "[to be translated]:刷新TTS服务类型设置", - "service_type.refreshed": "[to be translated]:已刷新TTS服务类型设置", - "api_key": "[to be translated]:API密钥", - "api_key.placeholder": "[to be translated]:请输入OpenAI API密钥", - "api_url": "[to be translated]:API地址", - "api_url.placeholder": "[to be translated]:例如:https://api.openai.com/v1/audio/speech", - "edge_voice": "[to be translated]:浏览器 TTS音色", - "edge_voice.loading": "[to be translated]:加载中...", - "edge_voice.refresh": "[to be translated]:刷新可用音色列表", - "edge_voice.not_found": "[to be translated]:未找到匹配的音色", - "voice": "[to be translated]:音色", - "voice.placeholder": "[to be translated]:请选择音色", - "voice_input_placeholder": "[to be translated]:输入音色", - "voice_add": "[to be translated]:添加", - "voice_empty": "[to be translated]:暂无自定义音色,请在下方添加", - "model": "[to be translated]:模型", - "model.placeholder": "[to be translated]:请选择模型", - "model_input_placeholder": "[to be translated]:输入模型", - "model_add": "[to be translated]:添加", - "model_empty": "[to be translated]:暂无自定义模型,请在下方添加", - "filter_options": "[to be translated]:过滤选项", - "filter.thinking_process": "[to be translated]:过滤思考过程", - "filter.markdown": "[to be translated]:过滤Markdown标记", - "filter.code_blocks": "[to be translated]:过滤代码块", - "filter.html_tags": "[to be translated]:过滤HTML标签", - "max_text_length": "[to be translated]:最大文本长度", - "service_type.siliconflow": "[to be translated]:硅基流动", - "service_type.mstts": "[to be translated]:免费在线 TTS", - "siliconflow_api_key": "[to be translated]:硅基流动API密钥", - "siliconflow_api_key.placeholder": "[to be translated]:请输入硅基流动API密钥", - "siliconflow_api_url": "[to be translated]:硅基流动API地址", - "siliconflow_api_url.placeholder": "[to be translated]:例如:https://api.siliconflow.cn/v1/audio/speech", - "siliconflow_voice": "[to be translated]:硅基流动音色", - "siliconflow_voice.placeholder": "[to be translated]:请选择音色", - "siliconflow_model": "[to be translated]:硅基流动模型", - "siliconflow_model.placeholder": "[to be translated]:请选择模型", - "siliconflow_response_format": "[to be translated]:响应格式", - "siliconflow_response_format.placeholder": "[to be translated]:默认为mp3", - "siliconflow_speed": "[to be translated]:语速", - "siliconflow_speed.placeholder": "[to be translated]:默认为1.0", - "edge_voice.available_count": "[to be translated]:可用语音: {{count}}个", - "edge_voice.refreshing": "[to be translated]:正在刷新语音列表...", - "edge_voice.refreshed": "[to be translated]:语音列表已刷新", - "mstts.voice": "[to be translated]:免费在线 TTS音色", - "mstts.output_format": "[to be translated]:输出格式", - "mstts.info": "[to be translated]:免费在线TTS服务不需要API密钥,完全免费使用。", - "error.no_mstts_voice": "[to be translated]:未设置免费在线 TTS音色", - "play": "[to be translated]:播放语音", - "stop": "[to be translated]:停止播放", - "speak": "[to be translated]:播放语音", - "stop_global": "[to be translated]:停止所有语音播放", - "stopped": "[to be translated]:已停止语音播放", - "segmented": "[to be translated]:分段", - "segmented_play": "[to be translated]:分段播放", - "segmented_playback": "[to be translated]:分段播放", - "filter.emojis": "[to be translated]:过滤表情符号", - "show_progress_bar": "[to be translated]:显示TTS进度条" + "tab_title": "語音合成", + "service_type.refresh": "刷新TTS服務類型設置", + "service_type.refreshed": "已刷新TTS服務類型設置", + "api_key": "API金鑰", + "api_key.placeholder": "請輸入OpenAI API金鑰", + "api_url": "API位址", + "api_url.placeholder": "例如:https://api.openai.com/v1/audio/speech", + "edge_voice": "瀏覽器 TTS音色", + "edge_voice.loading": "載入中...", + "edge_voice.refresh": "刷新可用音色列表", + "edge_voice.not_found": "未找到符合的音色", + "voice": "音色", + "voice.placeholder": "請選擇音色", + "voice_input_placeholder": "輸入音色", + "voice_add": "新增", + "voice_empty": "暫無自訂音色,請在下方新增", + "model": "模型", + "model.placeholder": "請選擇模型", + "model_input_placeholder": "輸入模型", + "model_add": "新增", + "model_empty": "暫無自訂模型,請在下方新增", + "filter_options": "篩選選項", + "filter.thinking_process": "篩選思考過程", + "filter.markdown": "篩選Markdown標記", + "filter.code_blocks": "篩選程式碼區塊", + "filter.html_tags": "篩選HTML標籤", + "max_text_length": "最大文字長度", + "service_type.siliconflow": "矽基流動", + "service_type.mstts": "免費線上 TTS", + "siliconflow_api_key": "矽基流動API金鑰", + "siliconflow_api_key.placeholder": "請輸入矽基流動API金鑰", + "siliconflow_api_url": "矽基流動API位址", + "siliconflow_api_url.placeholder": "例如:https://api.siliconflow.cn/v1/audio/speech", + "siliconflow_voice": "矽基流動音色", + "siliconflow_voice.placeholder": "請選擇音色", + "siliconflow_model": "矽基流動模型", + "siliconflow_model.placeholder": "請選擇模型", + "siliconflow_response_format": "回應格式", + "siliconflow_response_format.placeholder": "預設為mp3", + "siliconflow_speed": "語速", + "siliconflow_speed.placeholder": "預設為1.0", + "edge_voice.available_count": "可用語音: {{count}}個", + "edge_voice.refreshing": "正在刷新語音列表...", + "edge_voice.refreshed": "語音列表已刷新", + "mstts.voice": "免費線上 TTS音色", + "mstts.output_format": "輸出格式", + "mstts.info": "免費線上TTS服務不需要API金鑰,完全免費使用。", + "error.no_mstts_voice": "未設定免費線上 TTS音色", + "play": "播放語音", + "stop": "停止播放", + "speak": "播放語音", + "stop_global": "停止所有語音播放", + "stopped": "已停止語音播放", + "segmented": "分段", + "segmented_play": "分段播放", + "segmented_playback": "分段播放", + "filter.emojis": "篩選表情符號", + "show_progress_bar": "顯示TTS進度條" }, "voice": { - "title": "[to be translated]:语音功能", - "help": "[to be translated]:语音功能包括文本转语音(TTS)和语音识别(ASR)。", - "learn_more": "[to be translated]:了解更多" + "title": "語音功能", + "help": "語音功能包括文字轉語音(TTS)和語音辨識(ASR)。", + "learn_more": "了解更多" }, "asr": { - "title": "[to be translated]:语音识别", - "tab_title": "[to be translated]:语音识别", - "enable": "[to be translated]:启用语音识别", - "enable.help": "[to be translated]:启用后可以将语音转换为文本", - "service_type": "[to be translated]:服务类型", - "service_type.browser": "[to be translated]:浏览器", - "service_type.local": "[to be translated]:本地服务器", - "api_key": "[to be translated]:API密钥", - "api_key.placeholder": "[to be translated]:请输入OpenAI API密钥", - "api_url": "[to be translated]:API地址", - "api_url.placeholder": "[to be translated]:例如:https://api.openai.com/v1/audio/transcriptions", - "model": "[to be translated]:模型", - "browser.info": "[to be translated]:使用浏览器内置的语音识别功能,无需额外设置", - "local.info": "[to be translated]:使用本地服务器和浏览器进行语音识别,需要先启动服务器并打开浏览器页面", - "local.browser_tip": "[to be translated]:请在浏览器中打开此页面,并保持浏览器窗口打开", - "local.test_connection": "[to be translated]:测试连接", - "local.connection_success": "[to be translated]:连接成功", - "local.connection_failed": "[to be translated]:连接失败,请确保服务器已启动", - "server.start": "[to be translated]:启动服务器", - "server.stop": "[to be translated]:停止服务器", - "server.starting": "[to be translated]:正在启动服务器...", - "server.started": "[to be translated]:服务器已启动", - "server.stopping": "[to be translated]:正在停止服务器...", - "server.stopped": "[to be translated]:服务器已停止", - "server.already_running": "[to be translated]:服务器已经在运行中", - "server.not_running": "[to be translated]:服务器未运行", - "server.start_failed": "[to be translated]:启动服务器失败", - "server.stop_failed": "[to be translated]:停止服务器失败", - "open_browser": "[to be translated]:打开浏览器页面", - "test": "[to be translated]:测试语音识别", - "test_info": "[to be translated]:请在输入框中使用语音识别按钮进行测试", - "start": "[to be translated]:开始录音", - "stop": "[to be translated]:停止录音", - "preparing": "[to be translated]:准备中", - "recording": "[to be translated]:正在录音...", - "processing": "[to be translated]:正在处理语音...", - "success": "[to be translated]:语音识别成功", - "completed": "[to be translated]:语音识别完成", - "canceled": "[to be translated]:已取消录音", + "title": "語音辨識", + "tab_title": "語音辨識", + "enable": "啟用語音辨識", + "enable.help": "啟用後可以將語音轉換為文本", + "service_type": "服務類型", + "service_type.browser": "瀏覽器", + "service_type.local": "本地伺服器", + "api_key": "API金鑰", + "api_key.placeholder": "請輸入OpenAI API金鑰", + "api_url": "API位址", + "api_url.placeholder": "例如:https://api.openai.com/v1/audio/transcriptions", + "model": "模型", + "browser.info": "使用瀏覽器內建的語音辨識功能,無需額外設定", + "local.info": "使用本地伺服器和瀏覽器進行語音辨識,需要先啟動伺服器並開啟瀏覽器頁面", + "local.browser_tip": "請在瀏覽器中開啟此頁面,並保持瀏覽器視窗開啟", + "local.test_connection": "測試連接", + "local.connection_success": "連接成功", + "local.connection_failed": "連接失敗,請確保伺服器已啟動", + "server.start": "啟動伺服器", + "server.stop": "停止伺服器", + "server.starting": "正在啟動伺服器...", + "server.started": "伺服器已啟動", + "server.stopping": "正在停止伺服器...", + "server.stopped": "伺服器已停止", + "server.already_running": "伺服器已經在執行中", + "server.not_running": "伺服器未執行", + "server.start_failed": "啟動伺服器失敗", + "server.stop_failed": "停止伺服器失敗", + "open_browser": "開啟瀏覽器頁面", + "test": "測試語音辨識", + "test_info": "請在輸入框中使用語音辨識按鈕進行測試", + "start": "開始錄音", + "stop": "停止錄音", + "preparing": "準備中", + "recording": "正在錄音...", + "processing": "正在處理語音...", + "success": "語音辨識成功", + "completed": "語音辨識完成", + "canceled": "已取消錄音", "error": { - "not_enabled": "[to be translated]:语音识别功能未启用", - "no_api_key": "[to be translated]:未设置API密钥", - "browser_not_support": "[to be translated]:浏览器不支持语音识别", - "start_failed": "[to be translated]:开始录音失败", - "transcribe_failed": "[to be translated]:语音识别失败" + "not_enabled": "語音辨識功能未啟用", + "no_api_key": "未設定API金鑰", + "browser_not_support": "瀏覽器不支援語音辨識", + "start_failed": "開始錄音失敗", + "transcribe_failed": "語音辨識失敗" }, - "auto_start_server": "[to be translated]:启动应用自动开启服务器", - "auto_start_server.help": "[to be translated]:启用后,应用启动时会自动开启语音识别服务器" + "auto_start_server": "啟動應用程式自動開啟伺服器", + "auto_start_server.help": "啟用後,應用程式啟動時會自動開啟語音辨識伺服器" }, "voice_call": { - "tab_title": "[to be translated]:通话功能", - "enable": "[to be translated]:启用语音通话", - "enable.help": "[to be translated]:启用后可以使用语音通话功能与AI进行对话", - "model": "[to be translated]:通话模型", - "model.select": "[to be translated]:选择模型", - "model.current": "[to be translated]:当前模型: {{model}}", - "model.info": "[to be translated]:选择用于语音通话的AI模型,不同模型可能有不同的语音交互体验", + "tab_title": "通話功能", + "enable": "啟用語音通話", + "enable.help": "啟用後可以使用語音通話功能與AI進行對話", + "model": "通話模型", + "model.select": "選擇模型", + "model.current": "目前模型: {{model}}", + "model.info": "選擇用於語音通話的AI模型,不同模型可能有不同的語音互動體驗", "prompt": { "label": "語音通話提示詞", "placeholder": "請輸入語音通話提示詞", @@ -1537,7 +1537,7 @@ "asr_tts_info": "語音通話使用上面的語音識別(ASR)和語音合成(TTS)設置", "test": "測試通話", "test_info": "請使用輸入框右側的語音通話按鈕進行測試", - "welcome_message": "[to be translated]:您好,我是您的AI助手,请长按说话按钮进行对话。" + "welcome_message": "您好,我是您的AI助理,請長按說話按鈕進行對話。" } }, "translate": { @@ -1580,26 +1580,26 @@ "visualization": "視覺化" }, "voice_call": { - "title": "[to be translated]:语音通话", - "start": "[to be translated]:开始语音通话", - "end": "[to be translated]:结束通话", - "mute": "[to be translated]:静音", - "unmute": "[to be translated]:取消静音", - "pause": "[to be translated]:暂停", - "resume": "[to be translated]:继续", - "you": "[to be translated]:您", - "ai": "[to be translated]:AI", - "press_to_talk": "[to be translated]:长按说话", - "release_to_send": "[to be translated]:松开发送", - "initialization_failed": "[to be translated]:初始化语音通话失败", - "error": "[to be translated]:语音通话出错", - "initializing": "[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]:按下此快捷键开始录音,松开快捷键结束录音并发送" + "title": "語音通話", + "start": "開始語音通話", + "end": "結束通話", + "mute": "靜音", + "unmute": "取消靜音", + "pause": "暫停", + "resume": "繼續", + "you": "您", + "ai": "AI", + "press_to_talk": "長按說話", + "release_to_send": "放開傳送", + "initialization_failed": "初始化語音通話失敗", + "error": "語音通話出錯", + "initializing": "正在初始化語音通話...", + "ready": "語音通話已就緒", + "shortcut_key_setting": "語音辨識快速鍵設定", + "press_any_key": "請按任意鍵...", + "save": "儲存", + "cancel": "取消", + "shortcut_key_tip": "按下此快速鍵開始錄音,放開快速鍵結束錄音並傳送" } } } \ No newline at end of file From c6d9223e5c075862eb227e6d1d0c4c477cedfa78 Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:17:55 +0800 Subject: [PATCH 55/59] Update yarn.lock From dac4d4906e993cabb3120f87f2fa5d06e0e43466 Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:20:33 +0800 Subject: [PATCH 56/59] Update yarn.lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 1ed60241f8..e176240928 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3934,7 +3934,7 @@ __metadata: "@types/adm-zip": "npm:^0" "@types/diff": "npm:^7" "@types/fs-extra": "npm:^11" - "@types/js-yaml": "npm:^4", + "@types/js-yaml": "npm:^4.0.0", "@types/lodash": "npm:^4.17.16", "@types/markdown-it": "npm:^14" "@types/md5": "npm:^2.3.5" From c438ee9fa6ec102e608ce83de73b2883281e6bf3 Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:23:57 +0800 Subject: [PATCH 57/59] Update yarn.lock --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index e176240928..fd1afb6e7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3934,8 +3934,8 @@ __metadata: "@types/adm-zip": "npm:^0" "@types/diff": "npm:^7" "@types/fs-extra": "npm:^11" - "@types/js-yaml": "npm:^4.0.0", - "@types/lodash": "npm:^4.17.16", + "@types/js-yaml": "^4" + "@types/lodash": "^4.17.16" "@types/markdown-it": "npm:^14" "@types/md5": "npm:^2.3.5" "@types/node": "npm:^18.19.9" From 18b9a26dd4598472dbe4c99538e9a01cddb5c5c1 Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:38:13 +0800 Subject: [PATCH 58/59] Update Inputbar.tsx --- src/renderer/src/pages/home/Inputbar/Inputbar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 72280e3520..1230ef6fbe 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -1,3 +1,4 @@ +// @ts-ignore - Keep all imports for future use import { CodeOutlined, FileSearchOutlined, From a883f27c3f47cec6bde5c48586158a9d4f18e8ec Mon Sep 17 00:00:00 2001 From: 1600822305 <161661698+1600822305@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:41:14 +0800 Subject: [PATCH 59/59] Update Inputbar.tsx --- src/renderer/src/pages/home/Inputbar/Inputbar.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 1230ef6fbe..845ccaf909 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -1,12 +1,11 @@ -// @ts-ignore - Keep all imports for future use import { - CodeOutlined, - FileSearchOutlined, + CodeOutlined as _CodeOutlined, + FileSearchOutlined as _FileSearchOutlined, HolderOutlined, - PaperClipOutlined, - PauseCircleOutlined, - ThunderboltOutlined, - TranslationOutlined + PaperClipOutlined as _PaperClipOutlined, + PauseCircleOutlined as _PauseCircleOutlined, + ThunderboltOutlined as _ThunderboltOutlined, + TranslationOutlined as _TranslationOutlined } from '@ant-design/icons' import ASRButton from '@renderer/components/ASRButton' import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'