feat(engine): 优化 Vosk 字幕引擎支持

- 实现文件夹选择功能,用于选择 Vosk 模型路径
- 在 EngineControl 组件中添加模型路径选择按钮和相关提示
- 在 EngineStatus 组件中增加对空模型路径的检查和错误提示
This commit is contained in:
himeditator
2025-07-10 11:22:39 +08:00
parent 1c29fd5adc
commit d354a6fefa
11 changed files with 109 additions and 7 deletions

View File

@@ -1 +1 @@
from .process import mergeChunkChannels, resampleRawChunk
from .process import mergeChunkChannels, resampleRawChunk, resampleMonoChunk

View File

@@ -50,7 +50,7 @@ def resampleRawChunk(chunk, channels, orig_sr, target_sr, mode="sinc_best"):
def resampleMonoChunk(chunk, orig_sr, target_sr, mode="sinc_best"):
"""
将当前单通道进行重采样
将当前单通道音频块进行重采样
Args:
chunk: (bytes)单通道音频数据块

View File

@@ -9,11 +9,13 @@
- [x] 添加复制字幕到剪贴板功能 *2025/07/08*
- [x] 适配 macOS 平台 *2025/07/08*
- [x] 添加字幕文字描边 *2025/07/09*
- [x] 添加基于 Vosk 的字幕引擎 *2025/07/09*
## 待完成
- [ ] 添加 Ollama 模型用于本地字幕引擎的翻译
- [ ] 更新 README 和用户手册字幕引擎构建、Vosk 模型获取和使用)
- [ ] 添加本地字幕引擎
- [ ] 添加基于 Vosk 的字幕引擎
- [ ] 验证 / 添加基于 FunASR 的字幕引擎
- [ ] 减小软件不必要的体积

View File

@@ -44,6 +44,19 @@
- 发送:无数据
- 接收:`string`
### `control.folder.select`
**介绍:** 打开文件夹选择器,并将用户选择的文件夹路径返回给前端
**发起方:** 前端控制窗口
**接收方:** 后端控制窗口实例
**数据类型:**
- 发送:无数据
- 接收:`string`
## 前端 ==> 后端
### `control.uiLanguage.change`

View File

@@ -1,4 +1,4 @@
import { shell, BrowserWindow, ipcMain, nativeTheme } from 'electron'
import { shell, BrowserWindow, ipcMain, nativeTheme, dialog } from 'electron'
import path from 'path'
import { is } from '@electron-toolkit/utils'
import icon from '../../build/icon.png?asset'
@@ -72,6 +72,15 @@ class ControlWindow {
return allConfig.uiTheme
})
ipcMain.handle('control.folder.select', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory']
});
if (result.canceled) return "";
return result.filePaths[0];
})
ipcMain.on('control.uiLanguage.change', (_, args) => {
allConfig.uiLanguage = args
if(captionWindow.window){

View File

@@ -51,7 +51,12 @@
<a-card size="small" :title="$t('engine.showMore')" v-show="showMore">
<div class="input-item">
<span class="input-label">{{ $t('engine.apikey') }}</span>
<a-popover>
<template #content>
<p class="label-hover-info">{{ $t('engine.apikeyInfo') }}</p>
</template>
<span class="input-label info-label">{{ $t('engine.apikey') }}</span>
</a-popover>
<a-input
class="input-area"
type="password"
@@ -59,9 +64,19 @@
/>
</div>
<div class="input-item">
<span class="input-label">{{ $t('engine.modelPath') }}</span>
<a-popover>
<template #content>
<p class="label-hover-info">{{ $t('engine.modelPathInfo') }}</p>
</template>
<span class="input-label info-label">{{ $t('engine.modelPath') }}</span>
</a-popover>
<span
class="input-folder"
@click="selectFolderPath"
><span><FolderOpenOutlined /></span></span>
<a-input
class="input-area"
style="width:calc(100% - 140px);"
v-model:value="currentModelPath"
/>
</div>
@@ -106,7 +121,7 @@ import { storeToRefs } from 'pinia'
import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
import { useEngineControlStore } from '@renderer/stores/engineControl'
import { notification } from 'ant-design-vue'
import { InfoCircleOutlined } from '@ant-design/icons-vue';
import { FolderOpenOutlined ,InfoCircleOutlined } from '@ant-design/icons-vue';
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
@@ -168,6 +183,13 @@ function cancelChange(){
currentCustomizedCommand.value = engineControl.customizedCommand
}
function selectFolderPath() {
window.electron.ipcRenderer.invoke('control.folder.select').then((folderPath) => {
if(!folderPath) return
currentModelPath.value = folderPath
})
}
watch(changeSignal, (val) => {
if(val == true) {
cancelChange();
@@ -190,6 +212,35 @@ watch(currentEngine, (val) => {
<style scoped>
@import url(../assets/input.css);
.label-hover-info {
margin-top: 10px;
max-width: min(36vw, 380px);
}
.info-label {
color: #1677ff;
cursor: pointer;
}
.input-folder {
display:inline-block;
width: 40px;
font-size:1.38em;
cursor: pointer;
transition: all 0.25s;
}
.input-folder>span {
padding: 0 4px;
border: 2px solid #1677ff;
color: #1677ff;
border-radius: 30%;
}
.input-folder:hover {
transform: scale(1.1);
}
.customize-note {
padding: 10px 10px 0;
color: red;

View File

@@ -106,6 +106,11 @@ function openCaptionWindow() {
}
function startEngine() {
console.log(`@@${engineControl.modelPath}##`)
if(engineControl.modelPath.trim() === '') {
engineControl.emptyModelPathErr()
return
}
window.electron.ipcRenderer.send('control.engine.start')
}

View File

@@ -17,6 +17,8 @@ export default {
"custom": "Type: Custom engine, engine path: ",
"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.",
"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",
@@ -48,6 +50,9 @@ export default {
"enableTranslation": "Translation",
"showMore": "More Settings",
"apikey": "API KEY",
"modelPath": "Model Path",
"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.",
"customEngine": "Custom Engine",
custom: {
"title": "Custom Caption Engine",

View File

@@ -17,6 +17,8 @@ export default {
"custom": "タイプ:カスタムエンジン、エンジンパス:",
"args": "、コマンド引数:",
"pidInfo": "、字幕エンジンプロセス PID",
"empty": "モデルパスが空です",
"emptyInfo": "Vosk モデルのパスが空です。字幕エンジン設定の追加設定で Vosk モデルのパスを設定してください。",
"stopped": "字幕エンジンが停止しました",
"stoppedInfo": "字幕エンジンが停止しました。再起動するには「字幕エンジンを開始」ボタンをクリックしてください。",
"error": "エラーが発生しました",
@@ -48,6 +50,9 @@ export default {
"enableTranslation": "翻訳",
"showMore": "詳細設定",
"apikey": "API KEY",
"modelPath": "モデルパス",
"apikeyInfo": "Gummy 字幕エンジンに必要な API KEY は、アリババクラウド百煉プラットフォームから取得する必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。",
"modelPathInfo": "Vosk 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。",
"customEngine": "カスタムエンジン",
custom: {
"title": "カスタムキャプションエンジン",

View File

@@ -17,6 +17,8 @@ export default {
"custom": "类型:自定义引擎,引擎路径:",
"args": ",命令参数:",
"pidInfo": ",字幕引擎进程 PID",
"empty": "模型路径为空",
"emptyInfo": "Vosk 模型模型路径为空,请在字幕引擎设置的更多设置中设置 Vosk 模型的路径。",
"stopped": "字幕引擎停止",
"stoppedInfo": "字幕引擎已经停止,可点击“启动字幕引擎”按钮重新启动",
"error": "发生错误",
@@ -49,6 +51,8 @@ export default {
"showMore": "更多设置",
"apikey": "API KEY",
"modelPath": "模型路径",
"apikeyInfo": "Gummy 字幕引擎需要的 API KEY需要在阿里云百炼平台获取。详细信息见项目用户手册。",
"modelPathInfo": "Vosk 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。",
"customEngine": "自定义引擎",
custom: {
"title": "自定义字幕引擎",

View File

@@ -62,6 +62,13 @@ export const useEngineControlStore = defineStore('engineControl', () => {
changeSignal.value = true
}
function emptyModelPathErr() {
notification.open({
message: t('noti.empty'),
description: t('noti.emptyInfo')
});
}
window.electron.ipcRenderer.on('control.controls.set', (_, controls: Controls) => {
setControls(controls)
})
@@ -120,6 +127,7 @@ export const useEngineControlStore = defineStore('engineControl', () => {
customizedCommand, // 自定义字幕引擎的命令
setControls, // 设置引擎配置
sendControlsChange, // 发送最新控制消息到后端
emptyModelPathErr, // 模型路径为空时显示警告
changeSignal, // 配置改变信号
}
})