diff --git a/engine/main.py b/engine/main.py index 225c369..a99ca44 100644 --- a/engine/main.py +++ b/engine/main.py @@ -1,6 +1,7 @@ import wave import argparse import threading +import datetime from utils import stdout, stdout_cmd from utils import shared_data, start_server from utils import merge_chunk_channels, resample_chunk_mono @@ -10,36 +11,45 @@ from audio2text import SosvRecognizer from sysaudio import AudioStream -def audio_recording(stream: AudioStream, resample: bool, save = False, path = ''): +def audio_recording(stream: AudioStream, resample: bool, record = False, path = ''): global shared_data stream.open_stream() wf = None - if save: - if path != '': + full_name = '' + if record: + if path != '' and path[-1] != '/': path += '/' - wf = wave.open(f'{path}record.wav', 'wb') + cur_dt = datetime.datetime.now() + name = cur_dt.strftime("audio-%Y-%m-%dT%H-%M-%S") + full_name = f'{path}{name}.wav' + wf = wave.open(full_name, 'wb') wf.setnchannels(stream.CHANNELS) wf.setsampwidth(stream.SAMP_WIDTH) - wf.setframerate(stream.CHUNK_RATE) + wf.setframerate(stream.RATE) + stdout_cmd("info", "Audio recording...") while shared_data.status == 'running': raw_chunk = stream.read_chunk() - if save: wf.writeframes(raw_chunk) # type: ignore + if record: wf.writeframes(raw_chunk) # type: ignore if raw_chunk is None: continue if resample: chunk = resample_chunk_mono(raw_chunk, stream.CHANNELS, stream.RATE, 16000) else: chunk = merge_chunk_channels(raw_chunk, stream.CHANNELS) shared_data.chunk_queue.put(chunk) - if save: wf.close() # type: ignore + if record: + stdout_cmd("info", f"Audio saved to {full_name}") + wf.close() # type: ignore stream.close_stream_signal() -def main_gummy(s: str, t: str, a: int, c: int, k: str): +def main_gummy(s: str, t: str, a: int, c: int, k: str, r: bool, rp: str): """ Parameters: s: Source language t: Target language k: Aliyun Bailian API key + r: Whether to record the audio + rp: Path to save the recorded audio """ stream = AudioStream(a, c) if t == 'none': @@ -50,7 +60,7 @@ def main_gummy(s: str, t: str, a: int, c: int, k: str): engine.start() stream_thread = threading.Thread( target=audio_recording, - args=(stream, False), + args=(stream, False, r, rp), daemon=True ) stream_thread.start() @@ -61,7 +71,7 @@ def main_gummy(s: str, t: str, a: int, c: int, k: str): engine.stop() -def main_vosk(a: int, c: int, vosk: str, t: str, tm: str, omn: str): +def main_vosk(a: int, c: int, vosk: str, t: str, tm: str, omn: str, r: bool, rp: str): """ Parameters: a: Audio source: 0 for output, 1 for input @@ -70,6 +80,8 @@ def main_vosk(a: int, c: int, vosk: str, t: str, tm: str, omn: str): t: Target language tm: Translation model type, ollama or google omn: Ollama model name + r: Whether to record the audio + rp: Path to save the recorded audio """ stream = AudioStream(a, c) if t == 'none': @@ -80,7 +92,7 @@ def main_vosk(a: int, c: int, vosk: str, t: str, tm: str, omn: str): engine.start() stream_thread = threading.Thread( target=audio_recording, - args=(stream, True), + args=(stream, True, r, rp), daemon=True ) stream_thread.start() @@ -91,7 +103,7 @@ def main_vosk(a: int, c: int, vosk: str, t: str, tm: str, omn: str): engine.stop() -def main_sosv(a: int, c: int, sosv: str, s: str, t: str, tm: str, omn: str): +def main_sosv(a: int, c: int, sosv: str, s: str, t: str, tm: str, omn: str, r: bool, rp: str): """ Parameters: a: Audio source: 0 for output, 1 for input @@ -101,6 +113,8 @@ def main_sosv(a: int, c: int, sosv: str, s: str, t: str, tm: str, omn: str): t: Target language tm: Translation model type, ollama or google omn: Ollama model name + r: Whether to record the audio + rp: Path to save the recorded audio """ stream = AudioStream(a, c) if t == 'none': @@ -111,7 +125,7 @@ def main_sosv(a: int, c: int, sosv: str, s: str, t: str, tm: str, omn: str): engine.start() stream_thread = threading.Thread( target=audio_recording, - args=(stream, True), + args=(stream, True, r, rp), daemon=True ) stream_thread.start() @@ -130,6 +144,8 @@ if __name__ == "__main__": parser.add_argument('-c', '--chunk_rate', default=10, help='Number of audio stream chunks collected per second') parser.add_argument('-p', '--port', default=0, help='The port to run the server on, 0 for no server') parser.add_argument('-t', '--target_language', default='zh', help='Target language code, "none" for no translation') + parser.add_argument('-r', '--record', default=0, help='Whether to record the audio, 0 for no recording, 1 for recording') + parser.add_argument('-rp', '--record_path', default='', help='Path to save the recorded audio') # gummy and sosv parser.add_argument('-s', '--source_language', default='auto', help='Source language code') # gummy only @@ -154,7 +170,9 @@ if __name__ == "__main__": args.target_language, int(args.audio_type), int(args.chunk_rate), - args.api_key + args.api_key, + True if int(args.record) == 1 else False, + args.record_path ) elif args.caption_engine == 'vosk': main_vosk( @@ -163,7 +181,9 @@ if __name__ == "__main__": args.vosk_model, args.target_language, args.translation_model, - args.ollama_name + args.ollama_name, + True if int(args.record) == 1 else False, + args.record_path ) elif args.caption_engine == 'sosv': main_sosv( @@ -173,7 +193,9 @@ if __name__ == "__main__": args.source_language, args.target_language, args.translation_model, - args.ollama_name + args.ollama_name, + True if int(args.record) == 1 else False, + args.record_path ) else: raise ValueError('Invalid caption engine specified.') diff --git a/src/main/types/index.ts b/src/main/types/index.ts index b44fa2e..d80f142 100644 --- a/src/main/types/index.ts +++ b/src/main/types/index.ts @@ -11,8 +11,11 @@ export interface Controls { engine: string, audio: 0 | 1, translation: boolean, + recording: boolean, API_KEY: string, - modelPath: string, + voskModelPath: string, + sosvModelPath: string, + recordingPath: string, customized: boolean, customizedApp: string, customizedCommand: string, diff --git a/src/main/utils/AllConfig.ts b/src/main/utils/AllConfig.ts index 567d6b0..9e6109a 100644 --- a/src/main/utils/AllConfig.ts +++ b/src/main/utils/AllConfig.ts @@ -6,12 +6,18 @@ import { Log } from './Log' import { app, BrowserWindow } from 'electron' import * as path from 'path' import * as fs from 'fs' +import os from 'os' interface CaptionTranslation { time_s: string, translation: string } +function getDesktopPath() { + const homeDir = os.homedir() + return path.join(homeDir, 'Desktop') +} + const defaultStyles: Styles = { lineBreak: 1, fontFamily: 'sans-serif', @@ -42,8 +48,11 @@ const defaultControls: Controls = { audio: 0, engineEnabled: false, API_KEY: '', - modelPath: '', + voskModelPath: '', + sosvModelPath: '', + recordingPath: getDesktopPath(), translation: true, + recording: false, customized: false, customizedApp: '', customizedCommand: '', diff --git a/src/main/utils/CaptionEngine.ts b/src/main/utils/CaptionEngine.ts index 39e371a..1b876ff 100644 --- a/src/main/utils/CaptionEngine.ts +++ b/src/main/utils/CaptionEngine.ts @@ -63,8 +63,11 @@ export class CaptionEngine { this.appPath = path.join(process.resourcesPath, 'engine', 'main') } } - this.command.push('-a', allConfig.controls.audio ? '1' : '0') + if(allConfig.controls.recording) { + this.command.push('-r', '1') + this.command.push('-rp', allConfig.controls.recordingPath) + } this.port = Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024 this.command.push('-p', this.port.toString()) this.command.push( @@ -81,7 +84,14 @@ export class CaptionEngine { } else if(allConfig.controls.engine === 'vosk'){ this.command.push('-e', 'vosk') - this.command.push('-vosk', `"${allConfig.controls.modelPath}"`) + this.command.push('-vosk', `"${allConfig.controls.voskModelPath}"`) + this.command.push('-tm', allConfig.controls.transModel) + this.command.push('-omn', allConfig.controls.ollamaName) + } + else if(allConfig.controls.engine === 'sosv'){ + this.command.push('-e', 'sosv') + this.command.push('-s', allConfig.controls.sourceLang) + this.command.push('-sosv', `"${allConfig.controls.sosvModelPath}"`) this.command.push('-tm', allConfig.controls.transModel) this.command.push('-omn', allConfig.controls.ollamaName) } diff --git a/src/renderer/src/components/EngineControl.vue b/src/renderer/src/components/EngineControl.vue index fa19041..b6e04e8 100644 --- a/src/renderer/src/components/EngineControl.vue +++ b/src/renderer/src/components/EngineControl.vue @@ -19,7 +19,7 @@ :disabled="currentEngine === 'vosk'" class="input-area" v-model:value="currentSourceLang" - :options="langList" + :options="sLangList" >
@@ -27,7 +27,7 @@
@@ -65,9 +65,13 @@
- {{ $t('engine.customEngine') }} - + {{ $t('engine.enableRecording') }} +
+
+
+ {{ $t('engine.customEngine') }} +
{{ $t('engine.showMore') }} @@ -105,6 +109,9 @@ {{ $t('engine.modelPath') }} + >{{ $t('engine.voskModelPath') }} +
+
+ + + {{ $t('engine.sosvModelPath') }} + + + +
+
+ + + {{ $t('engine.recordingPath') }} + + +
@@ -183,19 +236,31 @@ const currentTargetLang = ref('zh') const currentEngine = ref('gummy') const currentAudio = ref<0 | 1>(0) const currentTranslation = ref(true) +const currentRecording = ref(false) const currentTransModel = ref('ollama') const currentOllamaName = ref('') const currentAPI_KEY = ref('') -const currentModelPath = ref('') +const currentVoskModelPath = ref('') +const currentSosvModelPath = ref('') +const currentRecordingPath = ref('') const currentCustomized = ref(false) const currentCustomizedApp = ref('') const currentCustomizedCommand = ref('') const currentStartTimeoutSeconds = ref(30) -const langList = computed(() => { +const sLangList = computed(() => { for(let item of captionEngine.value){ if(item.value === currentEngine.value) { - return item.languages + return item.languages.filter(item => item.type <= 0) + } + } + return [] +}) + +const tLangList = computed(() => { + for(let item of captionEngine.value){ + if(item.value === currentEngine.value) { + return item.languages.filter(item => item.type >= 0) } } return [] @@ -231,8 +296,11 @@ function applyChange(){ engineControl.engine = currentEngine.value engineControl.audio = currentAudio.value engineControl.translation = currentTranslation.value + engineControl.recording = currentRecording.value engineControl.API_KEY = currentAPI_KEY.value - engineControl.modelPath = currentModelPath.value + engineControl.voskModelPath = currentVoskModelPath.value + engineControl.sosvModelPath = currentSosvModelPath.value + engineControl.recordingPath = currentRecordingPath.value engineControl.customized = currentCustomized.value engineControl.customizedApp = currentCustomizedApp.value engineControl.customizedCommand = currentCustomizedCommand.value @@ -255,18 +323,26 @@ function cancelChange(){ currentEngine.value = engineControl.engine currentAudio.value = engineControl.audio currentTranslation.value = engineControl.translation + currentRecording.value = engineControl.recording currentAPI_KEY.value = engineControl.API_KEY - currentModelPath.value = engineControl.modelPath + currentVoskModelPath.value = engineControl.voskModelPath + currentSosvModelPath.value = engineControl.sosvModelPath + currentRecordingPath.value = engineControl.recordingPath currentCustomized.value = engineControl.customized currentCustomizedApp.value = engineControl.customizedApp currentCustomizedCommand.value = engineControl.customizedCommand currentStartTimeoutSeconds.value = engineControl.startTimeoutSeconds } -function selectFolderPath() { +function selectFolderPath(type: 'vosk' | 'sosv' | 'rec') { window.electron.ipcRenderer.invoke('control.folder.select').then((folderPath) => { if(!folderPath) return - currentModelPath.value = folderPath + if(type == 'vosk') + currentVoskModelPath.value = folderPath + else if(type == 'sosv') + currentSosvModelPath.value = folderPath + else if(type == 'rec') + currentRecordingPath.value = folderPath }) } @@ -285,7 +361,7 @@ watch(currentEngine, (val) => { currentTargetLang.value = 'zh-cn' } } - else if(val == 'gummy'){ + else{ currentSourceLang.value = 'auto' currentTargetLang.value = useGeneralSettingStore().uiLanguage } diff --git a/src/renderer/src/components/EngineStatus.vue b/src/renderer/src/components/EngineStatus.vue index a05a34a..416c7b2 100644 --- a/src/renderer/src/components/EngineStatus.vue +++ b/src/renderer/src/components/EngineStatus.vue @@ -175,7 +175,13 @@ function openCaptionWindow() { function startEngine() { pending.value = true isStarting.value = true - if(engineControl.engine === 'vosk' && engineControl.modelPath.trim() === '') { + if(engineControl.engine === 'vosk' && engineControl.voskModelPath.trim() === '') { + engineControl.emptyModelPathErr() + pending.value = false + isStarting.value = false + return + } + if(engineControl.engine === 'sosv' && engineControl.sosvModelPath.trim() === '') { engineControl.emptyModelPathErr() pending.value = false isStarting.value = false diff --git a/src/renderer/src/i18n/config/engine.ts b/src/renderer/src/i18n/config/engine.ts index dbad873..e8ff7e4 100644 --- a/src/renderer/src/i18n/config/engine.ts +++ b/src/renderer/src/i18n/config/engine.ts @@ -1,35 +1,58 @@ +// type: -1 仅为源语言,0 两者皆可, 1 仅为翻译语言 export const engines = { zh: [ { value: 'gummy', - label: '云端 - 阿里云 - Gummy', + label: '云端 / 阿里云 / Gummy', languages: [ - { value: 'auto', label: '自动检测' }, - { value: 'en', label: '英语' }, - { value: 'zh', label: '中文' }, - { value: 'ja', label: '日语' }, - { value: 'ko', label: '韩语' }, - { value: 'de', label: '德语' }, - { value: 'fr', label: '法语' }, - { value: 'ru', label: '俄语' }, - { value: 'es', label: '西班牙语' }, - { value: 'it', label: '意大利语' }, + { value: 'auto', type: -1, label: '自动检测' }, + { value: 'en', type: 0, label: '英语' }, + { value: 'zh', type: 0, label: '中文' }, + { value: 'ja', type: 0, label: '日语' }, + { value: 'ko', type: 0, label: '韩语' }, + { value: 'de', type: -1, label: '德语' }, + { value: 'fr', type: -1, label: '法语' }, + { value: 'ru', type: -1, label: '俄语' }, + { value: 'es', type: -1, label: '西班牙语' }, + { value: 'it', type: -1, label: '意大利语' }, + { value: 'yue', type: -1, label: '粤语' }, ] }, { value: 'vosk', - label: '本地 - Vosk', + label: '本地 / Vosk', languages: [ - { value: 'auto', label: '需要自行配置模型' }, - { value: 'en', label: '英语' }, - { value: 'zh-cn', label: '中文' }, - { value: 'ja', label: '日语' }, - { value: 'ko', label: '韩语' }, - { value: 'de', label: '德语' }, - { value: 'fr', label: '法语' }, - { value: 'ru', label: '俄语' }, - { value: 'es', label: '西班牙语' }, - { value: 'it', label: '意大利语' }, + { value: 'auto', type: -1, label: '需要自行配置模型' }, + { value: 'en', type: 1, label: '英语' }, + { value: 'zh-cn', type: 1, label: '中文' }, + { value: 'ja', type: 1, label: '日语' }, + { value: 'ko', type: 1, label: '韩语' }, + { value: 'de', type: 1, label: '德语' }, + { value: 'fr', type: 1, label: '法语' }, + { value: 'ru', type: 1, label: '俄语' }, + { value: 'es', type: 1, label: '西班牙语' }, + { value: 'it', type: 1, label: '意大利语' }, + ], + transModel: [ + { value: 'ollama', label: 'Ollama 本地模型' }, + { value: 'google', label: 'Google API 调用' }, + ] + }, + { + value: 'sosv', + label: '本地 / SOSV', + languages: [ + { value: 'auto', type: -1, label: '自动检测' }, + { value: 'en', type: 0, label: '英语' }, + { value: 'zh', type: 0, label: '中文' }, + { value: 'ja', type: 0, label: '日语' }, + { value: 'ko', type: 0, label: '韩语' }, + { value: 'yue', type: -1, label: '粤语' }, + { value: 'de', type: 1, label: '德语' }, + { value: 'fr', type: 1, label: '法语' }, + { value: 'ru', type: 1, label: '俄语' }, + { value: 'es', type: 1, label: '西班牙语' }, + { value: 'it', type: 1, label: '意大利语' }, ], transModel: [ { value: 'ollama', label: 'Ollama 本地模型' }, @@ -40,34 +63,56 @@ export const engines = { en: [ { value: 'gummy', - label: 'Cloud - Alibaba Cloud - Gummy', + label: 'Cloud / Alibaba Cloud / Gummy', languages: [ - { value: 'auto', label: 'Auto Detect' }, - { value: 'en', label: 'English' }, - { value: 'zh', label: 'Chinese' }, - { value: 'ja', label: 'Japanese' }, - { value: 'ko', label: 'Korean' }, - { value: 'de', label: 'German' }, - { value: 'fr', label: 'French' }, - { value: 'ru', label: 'Russian' }, - { value: 'es', label: 'Spanish' }, - { value: 'it', label: 'Italian' }, + { value: 'auto', type: -1, label: 'Auto Detect' }, + { value: 'en', type: 0, label: 'English' }, + { value: 'zh', type: 0, label: 'Chinese' }, + { value: 'ja', type: 0, label: 'Japanese' }, + { value: 'ko', type: 0, label: 'Korean' }, + { value: 'de', type: -1, label: 'German' }, + { value: 'fr', type: -1, label: 'French' }, + { value: 'ru', type: -1, label: 'Russian' }, + { value: 'es', type: -1, label: 'Spanish' }, + { value: 'it', type: -1, label: 'Italian' }, + { value: 'yue', type: -1, label: 'Cantonese' }, ] }, { value: 'vosk', - label: 'Local - Vosk', + label: 'Local / Vosk', languages: [ - { value: 'auto', label: 'Model needs to be configured manually' }, - { value: 'en', label: 'English' }, - { value: 'zh-cn', label: 'Chinese' }, - { value: 'ja', label: 'Japanese' }, - { value: 'ko', label: 'Korean' }, - { value: 'de', label: 'German' }, - { value: 'fr', label: 'French' }, - { value: 'ru', label: 'Russian' }, - { value: 'es', label: 'Spanish' }, - { value: 'it', label: 'Italian' }, + { value: 'auto', type: -1, label: 'Model needs to be configured manually' }, + { value: 'en', type: 1, label: 'English' }, + { value: 'zh-cn', type: 1, label: 'Chinese' }, + { value: 'ja', type: 1, label: 'Japanese' }, + { value: 'ko', type: 1, label: 'Korean' }, + { value: 'de', type: 1, label: 'German' }, + { value: 'fr', type: 1, label: 'French' }, + { value: 'ru', type: 1, label: 'Russian' }, + { value: 'es', type: 1, label: 'Spanish' }, + { value: 'it', type: 1, label: 'Italian' }, + ], + transModel: [ + { value: 'ollama', label: 'Ollama Local Model' }, + { value: 'google', label: 'Google API Call' }, + ] + }, + { + value: 'sosv', + label: 'Local / SOSV', + languages: [ + { value: 'auto', type: -1, label: 'Auto Detect' }, + { value: 'en', type: 0, label: 'English' }, + { value: 'zh-cn', type: 0, label: 'Chinese' }, + { value: 'ja', type: 0, label: 'Japanese' }, + { value: 'ko', type: 0, label: 'Korean' }, + { value: 'yue', type: -1, label: 'Cantonese' }, + { value: 'de', type: 1, label: 'German' }, + { value: 'fr', type: 1, label: 'French' }, + { value: 'ru', type: 1, label: 'Russian' }, + { value: 'es', type: 1, label: 'Spanish' }, + { value: 'it', type: 1, label: 'Italian' }, ], transModel: [ { value: 'ollama', label: 'Ollama Local Model' }, @@ -78,34 +123,56 @@ export const engines = { ja: [ { value: 'gummy', - label: 'クラウド - アリババクラウド - Gummy', + label: 'クラウド / アリババクラウド / Gummy', languages: [ - { value: 'auto', label: '自動検出' }, - { value: 'en', label: '英語' }, - { value: 'zh', label: '中国語' }, - { value: 'ja', label: '日本語' }, - { value: 'ko', label: '韓国語' }, - { value: 'de', label: 'ドイツ語' }, - { value: 'fr', label: 'フランス語' }, - { value: 'ru', label: 'ロシア語' }, - { value: 'es', label: 'スペイン語' }, - { value: 'it', label: 'イタリア語' }, + { value: 'auto', type: -1, label: '自動検出' }, + { value: 'en', type: 0, label: '英語' }, + { value: 'zh', type: 0, label: '中国語' }, + { value: 'ja', type: 0, label: '日本語' }, + { value: 'ko', type: 0, label: '韓国語' }, + { value: 'de', type: -1, label: 'ドイツ語' }, + { value: 'fr', type: -1, label: 'フランス語' }, + { value: 'ru', type: -1, label: 'ロシア語' }, + { value: 'es', type: -1, label: 'スペイン語' }, + { value: 'it', type: -1, label: 'イタリア語' }, + { value: 'yue', type: -1, label: '広東語' }, ] }, { value: 'vosk', - label: 'ローカル - Vosk', + label: 'ローカル / Vosk', languages: [ - { value: 'auto', label: 'モデルを手動で設定する必要があります' }, - { value: 'en', label: '英語' }, - { value: 'zh-cn', label: '中国語' }, - { value: 'ja', label: '日本語' }, - { value: 'ko', label: '韓国語' }, - { value: 'de', label: 'ドイツ語' }, - { value: 'fr', label: 'フランス語' }, - { value: 'ru', label: 'ロシア語' }, - { value: 'es', label: 'スペイン語' }, - { value: 'it', label: 'イタリア語' }, + { value: 'auto', type: -1, label: 'モデルを手動で設定する必要があります' }, + { value: 'en', type: 1, label: '英語' }, + { value: 'zh-cn', type: 1, label: '中国語' }, + { value: 'ja', type: 1, label: '日本語' }, + { value: 'ko', type: 1, label: '韓国語' }, + { value: 'de', type: 1, label: 'ドイツ語' }, + { value: 'fr', type: 1, label: 'フランス語' }, + { value: 'ru', type: 1, label: 'ロシア語' }, + { value: 'es', type: 1, label: 'スペイン語' }, + { value: 'it', type: 1, label: 'イタリア語' }, + ], + transModel: [ + { value: 'ollama', label: 'Ollama ローカルモデル' }, + { value: 'google', label: 'Google API 呼び出し' }, + ] + }, + { + value: 'sosv', + label: 'ローカル / SOSV', + languages: [ + { value: 'auto', type: -1, label: '自動検出' }, + { value: 'en', type: 0, label: '英語' }, + { value: 'zh-cn', type: 0, label: '中国語' }, + { value: 'ja', type: 0, label: '日本語' }, + { value: 'ko', type: 0, label: '韓国語' }, + { value: 'yue', type: -1, label: '広東語' }, + { value: 'de', type: 1, label: 'ドイツ語' }, + { value: 'fr', type: 1, label: 'フランス語' }, + { value: 'ru', type: 1, label: 'ロシア語' }, + { value: 'es', type: 1, label: 'スペイン語' }, + { value: 'it', type: 1, label: 'イタリア語' }, ], transModel: [ { value: 'ollama', label: 'Ollama ローカルモデル' }, diff --git a/src/renderer/src/i18n/lang/en.ts b/src/renderer/src/i18n/lang/en.ts index b6189e2..83216b9 100644 --- a/src/renderer/src/i18n/lang/en.ts +++ b/src/renderer/src/i18n/lang/en.ts @@ -18,7 +18,7 @@ export default { "args": ", command arguments: ", "pidInfo": ", caption engine process PID: ", "empty": "Model Path is Empty", - "emptyInfo": "The Vosk model path is empty. Please set the Vosk model path in the additional settings of the subtitle engine settings.", + "emptyInfo": "The model path for the selected local model is empty. Please download the corresponding model first, and then configure the model path in [Caption Engine Settings > More Settings].", "stopped": "Caption Engine Stopped", "stoppedInfo": "The caption engine has stopped. You can click the 'Start Caption Engine' button to restart it.", "error": "An error occurred", @@ -57,15 +57,22 @@ export default { "systemOutput": "System Audio Output (Speaker)", "systemInput": "System Audio Input (Microphone)", "enableTranslation": "Translation", + "enableRecording": "Enable Recording", "showMore": "More Settings", "apikey": "API KEY", - "modelPath": "Model Path", + "voskModelPath": "Vosk Path", + "sosvModelPath": "SOSV Path", + "recordingPath": "Save Path", "startTimeout": "Timeout", "seconds": "seconds", "apikeyInfo": "API KEY required for the Gummy subtitle engine, which needs to be obtained from the Alibaba Cloud Bailing platform. For more details, see the project user manual.", - "modelPathInfo": "The folder path of the model required by the Vosk subtitle engine. You need to download the required model to your local machine in advance. For more details, see the project user manual.", + "voskModelPathInfo": "The folder path of the model required by the Vosk subtitle engine. You need to download the required model to your local machine in advance. For more details, see the project user manual.", + "sosvModelPathInfo": "The folder path of the model required by the SOSV subtitle engine. You need to download the required model to your local machine in advance. For more details, see the project user manual.", + "recordingPathInfo": "The path to save recording files, requiring a folder path. The software will automatically name the recording file and save it as .wav file.", + "modelDownload": "Model Download Link", "startTimeoutInfo": "Caption engine startup timeout duration. Engine will be forcefully stopped if startup exceeds this time. Recommended range: 10-120 seconds.", - "customEngine": "Custom Engine", + "customEngine": "Custom Eng", + custom: { "title": "Custom Caption Engine", "attention": "Attention", diff --git a/src/renderer/src/i18n/lang/ja.ts b/src/renderer/src/i18n/lang/ja.ts index 1d7d23b..ad75bd5 100644 --- a/src/renderer/src/i18n/lang/ja.ts +++ b/src/renderer/src/i18n/lang/ja.ts @@ -18,7 +18,7 @@ export default { "args": "、コマンド引数:", "pidInfo": "、字幕エンジンプロセス PID:", "empty": "モデルパスが空です", - "emptyInfo": "Vosk モデルのパスが空です。字幕エンジン設定の追加設定で Vosk モデルのパスを設定してください。", + "emptyInfo": "選択されたローカルモデルに対応するモデルパスが空です。まず対応するモデルをダウンロードし、次に【字幕エンジン設定 > 詳細設定】でモデルのパスを設定してください。", "stopped": "字幕エンジンが停止しました", "stoppedInfo": "字幕エンジンが停止しました。再起動するには「字幕エンジンを開始」ボタンをクリックしてください。", "error": "エラーが発生しました", @@ -57,15 +57,21 @@ export default { "systemOutput": "システムオーディオ出力(スピーカー)", "systemInput": "システムオーディオ入力(マイク)", "enableTranslation": "翻訳", + "enableRecording": "録音", "showMore": "詳細設定", "apikey": "API KEY", - "modelPath": "モデルパス", + "voskModelPath": "Voskパス", + "sosvModelPath": "SOSVパス", + "recordingPath": "保存パス", "startTimeout": "時間制限", "seconds": "秒", "apikeyInfo": "Gummy 字幕エンジンに必要な API KEY は、アリババクラウド百煉プラットフォームから取得する必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", - "modelPathInfo": "Vosk 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", + "voskModelPathInfo": "Vosk 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", + "sosvModelPathInfo": "SOSV 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", + "recordingPathInfo": "録音ファイルの保存パスで、フォルダパスを指定する必要があります。ソフトウェアが自動的に録音ファイルに名前を付けて .wav ファイルとして保存します。", + "modelDownload": "モデルダウンロードリンク", "startTimeoutInfo": "字幕エンジンの起動タイムアウト時間です。この時間を超えると自動的に強制停止されます。10-120秒の範囲で設定することを推奨します。", - "customEngine": "カスタムエンジン", + "customEngine": "カスタム", custom: { "title": "カスタムキャプションエンジン", "attention": "注意事項", @@ -110,7 +116,7 @@ export default { "cpu": "CPU 使用率", "mem": "メモリ使用量", "elapsed": "稼働時間", - "customized": "カスタマイズ済み", + "customized": "カスタム", "status": "エンジン状態", "started": "開始済み", "stopped": "未開始", diff --git a/src/renderer/src/i18n/lang/zh.ts b/src/renderer/src/i18n/lang/zh.ts index e4a73c1..31bc52b 100644 --- a/src/renderer/src/i18n/lang/zh.ts +++ b/src/renderer/src/i18n/lang/zh.ts @@ -18,7 +18,7 @@ export default { "args": ",命令参数:", "pidInfo": ",字幕引擎进程 PID:", "empty": "模型路径为空", - "emptyInfo": "Vosk 模型模型路径为空,请在字幕引擎设置的更多设置中设置 Vosk 模型的路径。", + "emptyInfo": "选择的本地模型对应的模型路径为空。请先下载对应模型,然后在【字幕引擎设置 > 更多设置】中配置对应模型的路径。", "stopped": "字幕引擎停止", "stoppedInfo": "字幕引擎已经停止,可点击“启动字幕引擎”按钮重新启动", "error": "发生错误", @@ -57,13 +57,19 @@ export default { "systemOutput": "系统音频输出(扬声器)", "systemInput": "系统音频输入(麦克风)", "enableTranslation": "启用翻译", + "enableRecording": "启用录制", "showMore": "更多设置", "apikey": "API KEY", - "modelPath": "模型路径", + "voskModelPath": "Vosk路径", + "sosvModelPath": "SOSV路径", + "recordingPath": "保存路径", "startTimeout": "启动超时", "seconds": "秒", "apikeyInfo": "Gummy 字幕引擎需要的 API KEY,需要在阿里云百炼平台获取。详细信息见项目用户手册。", - "modelPathInfo": "Vosk 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。", + "voskModelPathInfo": "Vosk 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。", + "sosvModelPathInfo": "SOSV 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。", + "recordingPathInfo": "录音文件保存路径,需要提供文件夹路径。软件会自动命名录音文件并保存为 .wav 文件。", + "modelDownload": "模型下载地址", "startTimeoutInfo": "字幕引擎启动超时时间,超过此时间将自动强制停止。建议设置为 10-120 秒之间。", "customEngine": "自定义引擎", custom: { diff --git a/src/renderer/src/stores/engineControl.ts b/src/renderer/src/stores/engineControl.ts index 8810451..88e64a8 100644 --- a/src/renderer/src/stores/engineControl.ts +++ b/src/renderer/src/stores/engineControl.ts @@ -24,8 +24,11 @@ export const useEngineControlStore = defineStore('engineControl', () => { const engine = ref('gummy') const audio = ref<0 | 1>(0) const translation = ref(true) + const recording = ref(false) const API_KEY = ref('') - const modelPath = ref('') + const voskModelPath = ref('') + const sosvModelPath = ref('') + const recordingPath = ref('') const customized = ref(false) const customizedApp = ref('') const customizedCommand = ref('') @@ -44,8 +47,11 @@ export const useEngineControlStore = defineStore('engineControl', () => { engine: engine.value, audio: audio.value, translation: translation.value, + recording: recording.value, API_KEY: API_KEY.value, - modelPath: modelPath.value, + voskModelPath: voskModelPath.value, + sosvModelPath: sosvModelPath.value, + recordingPath: recordingPath.value, customized: customized.value, customizedApp: customizedApp.value, customizedCommand: customizedCommand.value, @@ -78,8 +84,11 @@ export const useEngineControlStore = defineStore('engineControl', () => { audio.value = controls.audio engineEnabled.value = controls.engineEnabled translation.value = controls.translation + recording.value = controls.recording API_KEY.value = controls.API_KEY - modelPath.value = controls.modelPath + voskModelPath.value = controls.voskModelPath + sosvModelPath.value = controls.sosvModelPath + recordingPath.value = controls.recordingPath customized.value = controls.customized customizedApp.value = controls.customizedApp customizedCommand.value = controls.customizedCommand @@ -89,9 +98,10 @@ export const useEngineControlStore = defineStore('engineControl', () => { function emptyModelPathErr() { notification.open({ - placement: 'topLeft', message: t('noti.empty'), - description: t('noti.emptyInfo') + description: t('noti.emptyInfo'), + duration: null, + icon: () => h(ExclamationCircleOutlined, { style: 'color: #ff4d4f' }) }); } @@ -139,12 +149,15 @@ export const useEngineControlStore = defineStore('engineControl', () => { sourceLang, // 源语言 targetLang, // 目标语言 transModel, // 翻译模型 - ollamaName, // Ollama 模型 + ollamaName, // Ollama 模型 engine, // 字幕引擎 audio, // 选择音频 translation, // 是否启用翻译 + recording, // 是否启用录音 API_KEY, // API KEY - modelPath, // vosk 模型路径 + voskModelPath, // vosk 模型路径 + sosvModelPath, // sosv 模型路径 + recordingPath, // 录音保存路径 customized, // 是否使用自定义字幕引擎 customizedApp, // 自定义字幕引擎的应用程序 customizedCommand, // 自定义字幕引擎的命令 diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index b44fa2e..d80f142 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -11,8 +11,11 @@ export interface Controls { engine: string, audio: 0 | 1, translation: boolean, + recording: boolean, API_KEY: string, - modelPath: string, + voskModelPath: string, + sosvModelPath: string, + recordingPath: string, customized: boolean, customizedApp: string, customizedCommand: string,