feat(renderer): 在用户界面中添加新功能的设置

- 添加录音功能,可保存为 WAV 文件
- 优化字幕引擎设置界面,支持更多配置选项
- 更新多语言翻译,增加模型下载链接等信息
This commit is contained in:
himeditator
2025-09-07 14:35:18 +08:00
parent 6bff978b88
commit 4abd6d0808
12 changed files with 351 additions and 123 deletions

View File

@@ -1,6 +1,7 @@
import wave import wave
import argparse import argparse
import threading import threading
import datetime
from utils import stdout, stdout_cmd from utils import stdout, stdout_cmd
from utils import shared_data, start_server from utils import shared_data, start_server
from utils import merge_chunk_channels, resample_chunk_mono from utils import merge_chunk_channels, resample_chunk_mono
@@ -10,36 +11,45 @@ from audio2text import SosvRecognizer
from sysaudio import AudioStream 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 global shared_data
stream.open_stream() stream.open_stream()
wf = None wf = None
if save: full_name = ''
if path != '': if record:
if path != '' and path[-1] != '/':
path += '/' 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.setnchannels(stream.CHANNELS)
wf.setsampwidth(stream.SAMP_WIDTH) wf.setsampwidth(stream.SAMP_WIDTH)
wf.setframerate(stream.CHUNK_RATE) wf.setframerate(stream.RATE)
stdout_cmd("info", "Audio recording...")
while shared_data.status == 'running': while shared_data.status == 'running':
raw_chunk = stream.read_chunk() 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 raw_chunk is None: continue
if resample: if resample:
chunk = resample_chunk_mono(raw_chunk, stream.CHANNELS, stream.RATE, 16000) chunk = resample_chunk_mono(raw_chunk, stream.CHANNELS, stream.RATE, 16000)
else: else:
chunk = merge_chunk_channels(raw_chunk, stream.CHANNELS) chunk = merge_chunk_channels(raw_chunk, stream.CHANNELS)
shared_data.chunk_queue.put(chunk) 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() 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: Parameters:
s: Source language s: Source language
t: Target language t: Target language
k: Aliyun Bailian API key k: Aliyun Bailian API key
r: Whether to record the audio
rp: Path to save the recorded audio
""" """
stream = AudioStream(a, c) stream = AudioStream(a, c)
if t == 'none': if t == 'none':
@@ -50,7 +60,7 @@ def main_gummy(s: str, t: str, a: int, c: int, k: str):
engine.start() engine.start()
stream_thread = threading.Thread( stream_thread = threading.Thread(
target=audio_recording, target=audio_recording,
args=(stream, False), args=(stream, False, r, rp),
daemon=True daemon=True
) )
stream_thread.start() stream_thread.start()
@@ -61,7 +71,7 @@ def main_gummy(s: str, t: str, a: int, c: int, k: str):
engine.stop() 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: Parameters:
a: Audio source: 0 for output, 1 for input 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 t: Target language
tm: Translation model type, ollama or google tm: Translation model type, ollama or google
omn: Ollama model name omn: Ollama model name
r: Whether to record the audio
rp: Path to save the recorded audio
""" """
stream = AudioStream(a, c) stream = AudioStream(a, c)
if t == 'none': if t == 'none':
@@ -80,7 +92,7 @@ def main_vosk(a: int, c: int, vosk: str, t: str, tm: str, omn: str):
engine.start() engine.start()
stream_thread = threading.Thread( stream_thread = threading.Thread(
target=audio_recording, target=audio_recording,
args=(stream, True), args=(stream, True, r, rp),
daemon=True daemon=True
) )
stream_thread.start() stream_thread.start()
@@ -91,7 +103,7 @@ def main_vosk(a: int, c: int, vosk: str, t: str, tm: str, omn: str):
engine.stop() 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: Parameters:
a: Audio source: 0 for output, 1 for input 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 t: Target language
tm: Translation model type, ollama or google tm: Translation model type, ollama or google
omn: Ollama model name omn: Ollama model name
r: Whether to record the audio
rp: Path to save the recorded audio
""" """
stream = AudioStream(a, c) stream = AudioStream(a, c)
if t == 'none': 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() engine.start()
stream_thread = threading.Thread( stream_thread = threading.Thread(
target=audio_recording, target=audio_recording,
args=(stream, True), args=(stream, True, r, rp),
daemon=True daemon=True
) )
stream_thread.start() 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('-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('-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('-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 # gummy and sosv
parser.add_argument('-s', '--source_language', default='auto', help='Source language code') parser.add_argument('-s', '--source_language', default='auto', help='Source language code')
# gummy only # gummy only
@@ -154,7 +170,9 @@ if __name__ == "__main__":
args.target_language, args.target_language,
int(args.audio_type), int(args.audio_type),
int(args.chunk_rate), 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': elif args.caption_engine == 'vosk':
main_vosk( main_vosk(
@@ -163,7 +181,9 @@ if __name__ == "__main__":
args.vosk_model, args.vosk_model,
args.target_language, args.target_language,
args.translation_model, 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': elif args.caption_engine == 'sosv':
main_sosv( main_sosv(
@@ -173,7 +193,9 @@ if __name__ == "__main__":
args.source_language, args.source_language,
args.target_language, args.target_language,
args.translation_model, args.translation_model,
args.ollama_name args.ollama_name,
True if int(args.record) == 1 else False,
args.record_path
) )
else: else:
raise ValueError('Invalid caption engine specified.') raise ValueError('Invalid caption engine specified.')

View File

@@ -11,8 +11,11 @@ export interface Controls {
engine: string, engine: string,
audio: 0 | 1, audio: 0 | 1,
translation: boolean, translation: boolean,
recording: boolean,
API_KEY: string, API_KEY: string,
modelPath: string, voskModelPath: string,
sosvModelPath: string,
recordingPath: string,
customized: boolean, customized: boolean,
customizedApp: string, customizedApp: string,
customizedCommand: string, customizedCommand: string,

View File

@@ -6,12 +6,18 @@ import { Log } from './Log'
import { app, BrowserWindow } from 'electron' import { app, BrowserWindow } from 'electron'
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs' import * as fs from 'fs'
import os from 'os'
interface CaptionTranslation { interface CaptionTranslation {
time_s: string, time_s: string,
translation: string translation: string
} }
function getDesktopPath() {
const homeDir = os.homedir()
return path.join(homeDir, 'Desktop')
}
const defaultStyles: Styles = { const defaultStyles: Styles = {
lineBreak: 1, lineBreak: 1,
fontFamily: 'sans-serif', fontFamily: 'sans-serif',
@@ -42,8 +48,11 @@ const defaultControls: Controls = {
audio: 0, audio: 0,
engineEnabled: false, engineEnabled: false,
API_KEY: '', API_KEY: '',
modelPath: '', voskModelPath: '',
sosvModelPath: '',
recordingPath: getDesktopPath(),
translation: true, translation: true,
recording: false,
customized: false, customized: false,
customizedApp: '', customizedApp: '',
customizedCommand: '', customizedCommand: '',

View File

@@ -63,8 +63,11 @@ export class CaptionEngine {
this.appPath = path.join(process.resourcesPath, 'engine', 'main') this.appPath = path.join(process.resourcesPath, 'engine', 'main')
} }
} }
this.command.push('-a', allConfig.controls.audio ? '1' : '0') 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.port = Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024
this.command.push('-p', this.port.toString()) this.command.push('-p', this.port.toString())
this.command.push( this.command.push(
@@ -81,7 +84,14 @@ export class CaptionEngine {
} }
else if(allConfig.controls.engine === 'vosk'){ else if(allConfig.controls.engine === 'vosk'){
this.command.push('-e', '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('-tm', allConfig.controls.transModel)
this.command.push('-omn', allConfig.controls.ollamaName) this.command.push('-omn', allConfig.controls.ollamaName)
} }

View File

@@ -19,7 +19,7 @@
:disabled="currentEngine === 'vosk'" :disabled="currentEngine === 'vosk'"
class="input-area" class="input-area"
v-model:value="currentSourceLang" v-model:value="currentSourceLang"
:options="langList" :options="sLangList"
></a-select> ></a-select>
</div> </div>
<div class="input-item"> <div class="input-item">
@@ -27,7 +27,7 @@
<a-select <a-select
class="input-area" class="input-area"
v-model:value="currentTargetLang" v-model:value="currentTargetLang"
:options="langList.filter((item) => item.value !== 'auto')" :options="tLangList"
></a-select> ></a-select>
</div> </div>
<div class="input-item" v-if="transModel"> <div class="input-item" v-if="transModel">
@@ -65,9 +65,13 @@
<a-switch v-model:checked="currentTranslation" /> <a-switch v-model:checked="currentTranslation" />
<span style="display:inline-block;width:10px;"></span> <span style="display:inline-block;width:10px;"></span>
<div style="display: inline-block;"> <div style="display: inline-block;">
<span class="switch-label">{{ $t('engine.customEngine') }}</span> <span class="switch-label">{{ $t('engine.enableRecording') }}</span>
<a-switch v-model:checked="currentCustomized" /> <a-switch v-model:checked="currentRecording" />
</div> </div>
</div>
<div class="input-item">
<span class="input-label">{{ $t('engine.customEngine') }}</span>
<a-switch v-model:checked="currentCustomized" />
<span style="display:inline-block;width:10px;"></span> <span style="display:inline-block;width:10px;"></span>
<div style="display: inline-block;"> <div style="display: inline-block;">
<span class="switch-label">{{ $t('engine.showMore') }}</span> <span class="switch-label">{{ $t('engine.showMore') }}</span>
@@ -105,6 +109,9 @@
<a-popover placement="right"> <a-popover placement="right">
<template #content> <template #content>
<p class="label-hover-info">{{ $t('engine.apikeyInfo') }}</p> <p class="label-hover-info">{{ $t('engine.apikeyInfo') }}</p>
<p><a href="https://bailian.console.aliyun.com" target="_blank">
https://bailian.console.aliyun.com
</a></p>
</template> </template>
<span class="input-label info-label" <span class="input-label info-label"
:style="{color: uiColor}" :style="{color: uiColor}"
@@ -119,21 +126,67 @@
<div class="input-item"> <div class="input-item">
<a-popover placement="right"> <a-popover placement="right">
<template #content> <template #content>
<p class="label-hover-info">{{ $t('engine.modelPathInfo') }}</p> <p class="label-hover-info">{{ $t('engine.voskModelPathInfo') }}</p>
<p class="label-hover-info">
<a href="https://alphacephei.com/vosk/models" target="_blank">Vosk {{ $t('engine.modelDownload') }}</a>
</p>
</template> </template>
<span class="input-label info-label" <span class="input-label info-label"
:style="{color: uiColor}" :style="{color: uiColor}"
>{{ $t('engine.modelPath') }}</span> >{{ $t('engine.voskModelPath') }}</span>
</a-popover> </a-popover>
<span <span
class="input-folder" class="input-folder"
:style="{color: uiColor}" :style="{color: uiColor}"
@click="selectFolderPath" @click="selectFolderPath('vosk')"
><span><FolderOpenOutlined /></span></span> ><span><FolderOpenOutlined /></span></span>
<a-input <a-input
class="input-area" class="input-area"
style="width:calc(100% - 140px);" style="width:calc(100% - 140px);"
v-model:value="currentModelPath" v-model:value="currentVoskModelPath"
/>
</div>
<div class="input-item">
<a-popover placement="right">
<template #content>
<p class="label-hover-info">{{ $t('engine.sosvModelPathInfo') }}</p>
<p class="label-hover-info">
<a href="https://github.com/HiMeditator/auto-caption/releases/tag/sosv-model" target="_blank">SOSV {{ $t('engine.modelDownload') }}</a>
</p>
</template>
<span class="input-label info-label"
:style="{color: uiColor}"
>{{ $t('engine.sosvModelPath') }}</span>
</a-popover>
<span
class="input-folder"
:style="{color: uiColor}"
@click="selectFolderPath('sosv')"
><span><FolderOpenOutlined /></span></span>
<a-input
class="input-area"
style="width:calc(100% - 140px);"
v-model:value="currentSosvModelPath"
/>
</div>
<div class="input-item">
<a-popover placement="right">
<template #content>
<p class="label-hover-info">{{ $t('engine.recordingPathInfo') }}</p>
</template>
<span class="input-label info-label"
:style="{color: uiColor}"
>{{ $t('engine.recordingPath') }}</span>
</a-popover>
<span
class="input-folder"
:style="{color: uiColor}"
@click="selectFolderPath('rec')"
><span><FolderOpenOutlined /></span></span>
<a-input
class="input-area"
style="width:calc(100% - 140px);"
v-model:value="currentRecordingPath"
/> />
</div> </div>
<div class="input-item"> <div class="input-item">
@@ -183,19 +236,31 @@ const currentTargetLang = ref('zh')
const currentEngine = ref<string>('gummy') const currentEngine = ref<string>('gummy')
const currentAudio = ref<0 | 1>(0) const currentAudio = ref<0 | 1>(0)
const currentTranslation = ref<boolean>(true) const currentTranslation = ref<boolean>(true)
const currentRecording = ref<boolean>(false)
const currentTransModel = ref('ollama') const currentTransModel = ref('ollama')
const currentOllamaName = ref('') const currentOllamaName = ref('')
const currentAPI_KEY = ref<string>('') const currentAPI_KEY = ref<string>('')
const currentModelPath = ref<string>('') const currentVoskModelPath = ref<string>('')
const currentSosvModelPath = ref<string>('')
const currentRecordingPath = ref<string>('')
const currentCustomized = ref<boolean>(false) const currentCustomized = ref<boolean>(false)
const currentCustomizedApp = ref('') const currentCustomizedApp = ref('')
const currentCustomizedCommand = ref('') const currentCustomizedCommand = ref('')
const currentStartTimeoutSeconds = ref<number>(30) const currentStartTimeoutSeconds = ref<number>(30)
const langList = computed(() => { const sLangList = computed(() => {
for(let item of captionEngine.value){ for(let item of captionEngine.value){
if(item.value === currentEngine.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 [] return []
@@ -231,8 +296,11 @@ function applyChange(){
engineControl.engine = currentEngine.value engineControl.engine = currentEngine.value
engineControl.audio = currentAudio.value engineControl.audio = currentAudio.value
engineControl.translation = currentTranslation.value engineControl.translation = currentTranslation.value
engineControl.recording = currentRecording.value
engineControl.API_KEY = currentAPI_KEY.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.customized = currentCustomized.value
engineControl.customizedApp = currentCustomizedApp.value engineControl.customizedApp = currentCustomizedApp.value
engineControl.customizedCommand = currentCustomizedCommand.value engineControl.customizedCommand = currentCustomizedCommand.value
@@ -255,18 +323,26 @@ function cancelChange(){
currentEngine.value = engineControl.engine currentEngine.value = engineControl.engine
currentAudio.value = engineControl.audio currentAudio.value = engineControl.audio
currentTranslation.value = engineControl.translation currentTranslation.value = engineControl.translation
currentRecording.value = engineControl.recording
currentAPI_KEY.value = engineControl.API_KEY 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 currentCustomized.value = engineControl.customized
currentCustomizedApp.value = engineControl.customizedApp currentCustomizedApp.value = engineControl.customizedApp
currentCustomizedCommand.value = engineControl.customizedCommand currentCustomizedCommand.value = engineControl.customizedCommand
currentStartTimeoutSeconds.value = engineControl.startTimeoutSeconds currentStartTimeoutSeconds.value = engineControl.startTimeoutSeconds
} }
function selectFolderPath() { function selectFolderPath(type: 'vosk' | 'sosv' | 'rec') {
window.electron.ipcRenderer.invoke('control.folder.select').then((folderPath) => { window.electron.ipcRenderer.invoke('control.folder.select').then((folderPath) => {
if(!folderPath) return 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' currentTargetLang.value = 'zh-cn'
} }
} }
else if(val == 'gummy'){ else{
currentSourceLang.value = 'auto' currentSourceLang.value = 'auto'
currentTargetLang.value = useGeneralSettingStore().uiLanguage currentTargetLang.value = useGeneralSettingStore().uiLanguage
} }

View File

@@ -175,7 +175,13 @@ function openCaptionWindow() {
function startEngine() { function startEngine() {
pending.value = true pending.value = true
isStarting.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() engineControl.emptyModelPathErr()
pending.value = false pending.value = false
isStarting.value = false isStarting.value = false

View File

@@ -1,35 +1,58 @@
// type: -1 仅为源语言0 两者皆可, 1 仅为翻译语言
export const engines = { export const engines = {
zh: [ zh: [
{ {
value: 'gummy', value: 'gummy',
label: '云端 - 阿里云 - Gummy', label: '云端 / 阿里云 / Gummy',
languages: [ languages: [
{ value: 'auto', label: '自动检测' }, { value: 'auto', type: -1, label: '自动检测' },
{ value: 'en', label: '英语' }, { value: 'en', type: 0, label: '英语' },
{ value: 'zh', label: '中文' }, { value: 'zh', type: 0, label: '中文' },
{ value: 'ja', label: '日语' }, { value: 'ja', type: 0, label: '日语' },
{ value: 'ko', label: '韩语' }, { value: 'ko', type: 0, label: '韩语' },
{ value: 'de', label: '德语' }, { value: 'de', type: -1, label: '德语' },
{ value: 'fr', label: '法语' }, { value: 'fr', type: -1, label: '法语' },
{ value: 'ru', label: '俄语' }, { value: 'ru', type: -1, label: '俄语' },
{ value: 'es', label: '西班牙语' }, { value: 'es', type: -1, label: '西班牙语' },
{ value: 'it', label: '意大利语' }, { value: 'it', type: -1, label: '意大利语' },
{ value: 'yue', type: -1, label: '粤语' },
] ]
}, },
{ {
value: 'vosk', value: 'vosk',
label: '本地 - Vosk', label: '本地 / Vosk',
languages: [ languages: [
{ value: 'auto', label: '需要自行配置模型' }, { value: 'auto', type: -1, label: '需要自行配置模型' },
{ value: 'en', label: '英语' }, { value: 'en', type: 1, label: '英语' },
{ value: 'zh-cn', label: '中文' }, { value: 'zh-cn', type: 1, label: '中文' },
{ value: 'ja', label: '日语' }, { value: 'ja', type: 1, label: '日语' },
{ value: 'ko', label: '韩语' }, { value: 'ko', type: 1, label: '韩语' },
{ value: 'de', label: '德语' }, { value: 'de', type: 1, label: '德语' },
{ value: 'fr', label: '法语' }, { value: 'fr', type: 1, label: '法语' },
{ value: 'ru', label: '俄语' }, { value: 'ru', type: 1, label: '俄语' },
{ value: 'es', label: '西班牙语' }, { value: 'es', type: 1, label: '西班牙语' },
{ value: 'it', 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: [ transModel: [
{ value: 'ollama', label: 'Ollama 本地模型' }, { value: 'ollama', label: 'Ollama 本地模型' },
@@ -40,34 +63,56 @@ export const engines = {
en: [ en: [
{ {
value: 'gummy', value: 'gummy',
label: 'Cloud - Alibaba Cloud - Gummy', label: 'Cloud / Alibaba Cloud / Gummy',
languages: [ languages: [
{ value: 'auto', label: 'Auto Detect' }, { value: 'auto', type: -1, label: 'Auto Detect' },
{ value: 'en', label: 'English' }, { value: 'en', type: 0, label: 'English' },
{ value: 'zh', label: 'Chinese' }, { value: 'zh', type: 0, label: 'Chinese' },
{ value: 'ja', label: 'Japanese' }, { value: 'ja', type: 0, label: 'Japanese' },
{ value: 'ko', label: 'Korean' }, { value: 'ko', type: 0, label: 'Korean' },
{ value: 'de', label: 'German' }, { value: 'de', type: -1, label: 'German' },
{ value: 'fr', label: 'French' }, { value: 'fr', type: -1, label: 'French' },
{ value: 'ru', label: 'Russian' }, { value: 'ru', type: -1, label: 'Russian' },
{ value: 'es', label: 'Spanish' }, { value: 'es', type: -1, label: 'Spanish' },
{ value: 'it', label: 'Italian' }, { value: 'it', type: -1, label: 'Italian' },
{ value: 'yue', type: -1, label: 'Cantonese' },
] ]
}, },
{ {
value: 'vosk', value: 'vosk',
label: 'Local - Vosk', label: 'Local / Vosk',
languages: [ languages: [
{ value: 'auto', label: 'Model needs to be configured manually' }, { value: 'auto', type: -1, label: 'Model needs to be configured manually' },
{ value: 'en', label: 'English' }, { value: 'en', type: 1, label: 'English' },
{ value: 'zh-cn', label: 'Chinese' }, { value: 'zh-cn', type: 1, label: 'Chinese' },
{ value: 'ja', label: 'Japanese' }, { value: 'ja', type: 1, label: 'Japanese' },
{ value: 'ko', label: 'Korean' }, { value: 'ko', type: 1, label: 'Korean' },
{ value: 'de', label: 'German' }, { value: 'de', type: 1, label: 'German' },
{ value: 'fr', label: 'French' }, { value: 'fr', type: 1, label: 'French' },
{ value: 'ru', label: 'Russian' }, { value: 'ru', type: 1, label: 'Russian' },
{ value: 'es', label: 'Spanish' }, { value: 'es', type: 1, label: 'Spanish' },
{ value: 'it', label: 'Italian' }, { 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: [ transModel: [
{ value: 'ollama', label: 'Ollama Local Model' }, { value: 'ollama', label: 'Ollama Local Model' },
@@ -78,34 +123,56 @@ export const engines = {
ja: [ ja: [
{ {
value: 'gummy', value: 'gummy',
label: 'クラウド - アリババクラウド - Gummy', label: 'クラウド / アリババクラウド / Gummy',
languages: [ languages: [
{ value: 'auto', label: '自動検出' }, { value: 'auto', type: -1, label: '自動検出' },
{ value: 'en', label: '英語' }, { value: 'en', type: 0, label: '英語' },
{ value: 'zh', label: '中国語' }, { value: 'zh', type: 0, label: '中国語' },
{ value: 'ja', label: '日本語' }, { value: 'ja', type: 0, label: '日本語' },
{ value: 'ko', label: '韓国語' }, { value: 'ko', type: 0, label: '韓国語' },
{ value: 'de', label: 'ドイツ語' }, { value: 'de', type: -1, label: 'ドイツ語' },
{ value: 'fr', label: 'フランス語' }, { value: 'fr', type: -1, label: 'フランス語' },
{ value: 'ru', label: 'ロシア語' }, { value: 'ru', type: -1, label: 'ロシア語' },
{ value: 'es', label: 'スペイン語' }, { value: 'es', type: -1, label: 'スペイン語' },
{ value: 'it', label: 'イタリア語' }, { value: 'it', type: -1, label: 'イタリア語' },
{ value: 'yue', type: -1, label: '広東語' },
] ]
}, },
{ {
value: 'vosk', value: 'vosk',
label: 'ローカル - Vosk', label: 'ローカル / Vosk',
languages: [ languages: [
{ value: 'auto', label: 'モデルを手動で設定する必要があります' }, { value: 'auto', type: -1, label: 'モデルを手動で設定する必要があります' },
{ value: 'en', label: '英語' }, { value: 'en', type: 1, label: '英語' },
{ value: 'zh-cn', label: '中国語' }, { value: 'zh-cn', type: 1, label: '中国語' },
{ value: 'ja', label: '日本語' }, { value: 'ja', type: 1, label: '日本語' },
{ value: 'ko', label: '韓国語' }, { value: 'ko', type: 1, label: '韓国語' },
{ value: 'de', label: 'ドイツ語' }, { value: 'de', type: 1, label: 'ドイツ語' },
{ value: 'fr', label: 'フランス語' }, { value: 'fr', type: 1, label: 'フランス語' },
{ value: 'ru', label: 'ロシア語' }, { value: 'ru', type: 1, label: 'ロシア語' },
{ value: 'es', label: 'スペイン語' }, { value: 'es', type: 1, label: 'スペイン語' },
{ value: 'it', 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: [ transModel: [
{ value: 'ollama', label: 'Ollama ローカルモデル' }, { value: 'ollama', label: 'Ollama ローカルモデル' },

View File

@@ -18,7 +18,7 @@ export default {
"args": ", command arguments: ", "args": ", command arguments: ",
"pidInfo": ", caption engine process PID: ", "pidInfo": ", caption engine process PID: ",
"empty": "Model Path is Empty", "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", "stopped": "Caption Engine Stopped",
"stoppedInfo": "The caption engine has stopped. You can click the 'Start Caption Engine' button to restart it.", "stoppedInfo": "The caption engine has stopped. You can click the 'Start Caption Engine' button to restart it.",
"error": "An error occurred", "error": "An error occurred",
@@ -57,15 +57,22 @@ export default {
"systemOutput": "System Audio Output (Speaker)", "systemOutput": "System Audio Output (Speaker)",
"systemInput": "System Audio Input (Microphone)", "systemInput": "System Audio Input (Microphone)",
"enableTranslation": "Translation", "enableTranslation": "Translation",
"enableRecording": "Enable Recording",
"showMore": "More Settings", "showMore": "More Settings",
"apikey": "API KEY", "apikey": "API KEY",
"modelPath": "Model Path", "voskModelPath": "Vosk Path",
"sosvModelPath": "SOSV Path",
"recordingPath": "Save Path",
"startTimeout": "Timeout", "startTimeout": "Timeout",
"seconds": "seconds", "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.", "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.", "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: { custom: {
"title": "Custom Caption Engine", "title": "Custom Caption Engine",
"attention": "Attention", "attention": "Attention",

View File

@@ -18,7 +18,7 @@ export default {
"args": "、コマンド引数:", "args": "、コマンド引数:",
"pidInfo": "、字幕エンジンプロセス PID", "pidInfo": "、字幕エンジンプロセス PID",
"empty": "モデルパスが空です", "empty": "モデルパスが空です",
"emptyInfo": "Vosk モデルのパスが空です。字幕エンジン設定の追加設定で Vosk モデルのパスを設定してください。", "emptyInfo": "選択されたローカルモデルに対応するモデルパスが空です。まず対応するモデルをダウンロードし、次に【字幕エンジン設定 > 詳細設定】でモデルのパスを設定してください。",
"stopped": "字幕エンジンが停止しました", "stopped": "字幕エンジンが停止しました",
"stoppedInfo": "字幕エンジンが停止しました。再起動するには「字幕エンジンを開始」ボタンをクリックしてください。", "stoppedInfo": "字幕エンジンが停止しました。再起動するには「字幕エンジンを開始」ボタンをクリックしてください。",
"error": "エラーが発生しました", "error": "エラーが発生しました",
@@ -57,15 +57,21 @@ export default {
"systemOutput": "システムオーディオ出力(スピーカー)", "systemOutput": "システムオーディオ出力(スピーカー)",
"systemInput": "システムオーディオ入力(マイク)", "systemInput": "システムオーディオ入力(マイク)",
"enableTranslation": "翻訳", "enableTranslation": "翻訳",
"enableRecording": "録音",
"showMore": "詳細設定", "showMore": "詳細設定",
"apikey": "API KEY", "apikey": "API KEY",
"modelPath": "モデルパス", "voskModelPath": "Voskパス",
"sosvModelPath": "SOSVパス",
"recordingPath": "保存パス",
"startTimeout": "時間制限", "startTimeout": "時間制限",
"seconds": "秒", "seconds": "秒",
"apikeyInfo": "Gummy 字幕エンジンに必要な API KEY は、アリババクラウド百煉プラットフォームから取得する必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", "apikeyInfo": "Gummy 字幕エンジンに必要な API KEY は、アリババクラウド百煉プラットフォームから取得する必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。",
"modelPathInfo": "Vosk 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", "voskModelPathInfo": "Vosk 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。",
"sosvModelPathInfo": "SOSV 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。",
"recordingPathInfo": "録音ファイルの保存パスで、フォルダパスを指定する必要があります。ソフトウェアが自動的に録音ファイルに名前を付けて .wav ファイルとして保存します。",
"modelDownload": "モデルダウンロードリンク",
"startTimeoutInfo": "字幕エンジンの起動タイムアウト時間です。この時間を超えると自動的に強制停止されます。10-120秒の範囲で設定することを推奨します。", "startTimeoutInfo": "字幕エンジンの起動タイムアウト時間です。この時間を超えると自動的に強制停止されます。10-120秒の範囲で設定することを推奨します。",
"customEngine": "カスタムエンジン", "customEngine": "カスタム",
custom: { custom: {
"title": "カスタムキャプションエンジン", "title": "カスタムキャプションエンジン",
"attention": "注意事項", "attention": "注意事項",
@@ -110,7 +116,7 @@ export default {
"cpu": "CPU 使用率", "cpu": "CPU 使用率",
"mem": "メモリ使用量", "mem": "メモリ使用量",
"elapsed": "稼働時間", "elapsed": "稼働時間",
"customized": "カスタマイズ済み", "customized": "カスタ",
"status": "エンジン状態", "status": "エンジン状態",
"started": "開始済み", "started": "開始済み",
"stopped": "未開始", "stopped": "未開始",

View File

@@ -18,7 +18,7 @@ export default {
"args": ",命令参数:", "args": ",命令参数:",
"pidInfo": ",字幕引擎进程 PID", "pidInfo": ",字幕引擎进程 PID",
"empty": "模型路径为空", "empty": "模型路径为空",
"emptyInfo": "Vosk 模型模型路径为空,请在字幕引擎设置更多设置中设置 Vosk 模型的路径。", "emptyInfo": "选择的本地模型对应的模型路径为空。请先下载对应模型,然后在【字幕引擎设置 > 更多设置】中配置对应模型的路径。",
"stopped": "字幕引擎停止", "stopped": "字幕引擎停止",
"stoppedInfo": "字幕引擎已经停止,可点击“启动字幕引擎”按钮重新启动", "stoppedInfo": "字幕引擎已经停止,可点击“启动字幕引擎”按钮重新启动",
"error": "发生错误", "error": "发生错误",
@@ -57,13 +57,19 @@ export default {
"systemOutput": "系统音频输出(扬声器)", "systemOutput": "系统音频输出(扬声器)",
"systemInput": "系统音频输入(麦克风)", "systemInput": "系统音频输入(麦克风)",
"enableTranslation": "启用翻译", "enableTranslation": "启用翻译",
"enableRecording": "启用录制",
"showMore": "更多设置", "showMore": "更多设置",
"apikey": "API KEY", "apikey": "API KEY",
"modelPath": "模型路径", "voskModelPath": "Vosk路径",
"sosvModelPath": "SOSV路径",
"recordingPath": "保存路径",
"startTimeout": "启动超时", "startTimeout": "启动超时",
"seconds": "秒", "seconds": "秒",
"apikeyInfo": "Gummy 字幕引擎需要的 API KEY需要在阿里云百炼平台获取。详细信息见项目用户手册。", "apikeyInfo": "Gummy 字幕引擎需要的 API KEY需要在阿里云百炼平台获取。详细信息见项目用户手册。",
"modelPathInfo": "Vosk 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。", "voskModelPathInfo": "Vosk 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。",
"sosvModelPathInfo": "SOSV 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。",
"recordingPathInfo": "录音文件保存路径,需要提供文件夹路径。软件会自动命名录音文件并保存为 .wav 文件。",
"modelDownload": "模型下载地址",
"startTimeoutInfo": "字幕引擎启动超时时间,超过此时间将自动强制停止。建议设置为 10-120 秒之间。", "startTimeoutInfo": "字幕引擎启动超时时间,超过此时间将自动强制停止。建议设置为 10-120 秒之间。",
"customEngine": "自定义引擎", "customEngine": "自定义引擎",
custom: { custom: {

View File

@@ -24,8 +24,11 @@ export const useEngineControlStore = defineStore('engineControl', () => {
const engine = ref<string>('gummy') const engine = ref<string>('gummy')
const audio = ref<0 | 1>(0) const audio = ref<0 | 1>(0)
const translation = ref<boolean>(true) const translation = ref<boolean>(true)
const recording = ref<boolean>(false)
const API_KEY = ref<string>('') const API_KEY = ref<string>('')
const modelPath = ref<string>('') const voskModelPath = ref<string>('')
const sosvModelPath = ref<string>('')
const recordingPath = ref<string>('')
const customized = ref<boolean>(false) const customized = ref<boolean>(false)
const customizedApp = ref<string>('') const customizedApp = ref<string>('')
const customizedCommand = ref<string>('') const customizedCommand = ref<string>('')
@@ -44,8 +47,11 @@ export const useEngineControlStore = defineStore('engineControl', () => {
engine: engine.value, engine: engine.value,
audio: audio.value, audio: audio.value,
translation: translation.value, translation: translation.value,
recording: recording.value,
API_KEY: API_KEY.value, API_KEY: API_KEY.value,
modelPath: modelPath.value, voskModelPath: voskModelPath.value,
sosvModelPath: sosvModelPath.value,
recordingPath: recordingPath.value,
customized: customized.value, customized: customized.value,
customizedApp: customizedApp.value, customizedApp: customizedApp.value,
customizedCommand: customizedCommand.value, customizedCommand: customizedCommand.value,
@@ -78,8 +84,11 @@ export const useEngineControlStore = defineStore('engineControl', () => {
audio.value = controls.audio audio.value = controls.audio
engineEnabled.value = controls.engineEnabled engineEnabled.value = controls.engineEnabled
translation.value = controls.translation translation.value = controls.translation
recording.value = controls.recording
API_KEY.value = controls.API_KEY 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 customized.value = controls.customized
customizedApp.value = controls.customizedApp customizedApp.value = controls.customizedApp
customizedCommand.value = controls.customizedCommand customizedCommand.value = controls.customizedCommand
@@ -89,9 +98,10 @@ export const useEngineControlStore = defineStore('engineControl', () => {
function emptyModelPathErr() { function emptyModelPathErr() {
notification.open({ notification.open({
placement: 'topLeft',
message: t('noti.empty'), 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, // 源语言 sourceLang, // 源语言
targetLang, // 目标语言 targetLang, // 目标语言
transModel, // 翻译模型 transModel, // 翻译模型
ollamaName, // Ollama 模型 ollamaName, // Ollama 模型
engine, // 字幕引擎 engine, // 字幕引擎
audio, // 选择音频 audio, // 选择音频
translation, // 是否启用翻译 translation, // 是否启用翻译
recording, // 是否启用录音
API_KEY, // API KEY API_KEY, // API KEY
modelPath, // vosk 模型路径 voskModelPath, // vosk 模型路径
sosvModelPath, // sosv 模型路径
recordingPath, // 录音保存路径
customized, // 是否使用自定义字幕引擎 customized, // 是否使用自定义字幕引擎
customizedApp, // 自定义字幕引擎的应用程序 customizedApp, // 自定义字幕引擎的应用程序
customizedCommand, // 自定义字幕引擎的命令 customizedCommand, // 自定义字幕引擎的命令

View File

@@ -11,8 +11,11 @@ export interface Controls {
engine: string, engine: string,
audio: 0 | 1, audio: 0 | 1,
translation: boolean, translation: boolean,
recording: boolean,
API_KEY: string, API_KEY: string,
modelPath: string, voskModelPath: string,
sosvModelPath: string,
recordingPath: string,
customized: boolean, customized: boolean,
customizedApp: string, customizedApp: string,
customizedCommand: string, customizedCommand: string,